Features / Extend

Three storage backends

Files, SQLite or Postgres — same API, same schemas, same test suite. You pick by how you operate, not by what you need, and switch later with a verified migration in either direction.

Where your content lives is an operations question: one instance or several, content in Git or in a database. RustyCMS does not make it a product question: three backends sit behind the same REST API, the same schemas and the same conformance suite. And the decision is not final.

Files: the default, diffable in Git

<content_dir>/<env>/<collection>/<slug>.json5

One entry, one file. git log says who changed what and when, a pull request shows a text diff, rsync pushes the tree to the server. No service, no backup beyond the repo. Writes are atomic: temp file, fsync, rename.

The limit is real: a filtered, sorted list reads the whole collection. Up to roughly 1000 entries that beats a database roundtrip; at 10,000 the same list costs 1.4 s instead of 2 ms. And two processes on one directory overwrite each other.

SQLite: one instance, many entries

One database file per space. Filter, sort and paging run as SQL instead of a scan, without a database server. The price: content is binary — no diff, no review in a PR.

Postgres: several instances

One database for all spaces, built for containers behind a load balancer, blue/green deploys, managed hosting. Every write announces itself via LISTEN/NOTIFY and all instances drop their cache. Background jobs take a per-tick database lock, so only one instance runs them. Edit locks hold across instances.

Plus what only a database can do: filters, facets and reference joins run as SQL — 50 hits with one reference field cost 2 statements instead of 51. index: true creates sort indexes: 100,000 rows sorted with paging, 17.6 → 0.74 ms. Read views open the content to SQL and BI tools. Full-text search runs as tsvector in the entry’s transaction. On writes, Postgres is about 15 times faster than file mode.

Switch, with proof

# without --apply: dry run, nothing is written
store-migrate --from file --to postgres --content-dir ./content \
  --database-url postgres://… --apply

After copying, the tool reads the target back and compares per-collection counts and a sha256 over canonical JSON per entry. On any difference the run fails and the mode stays as it was. --to file works the same — an archive readable in five years without Postgres. The mode is pinned in _config/store.json5; a conflicting configuration aborts the start instead of quietly booting an empty store.

Honestly: which one

File mode remains the recommendation. With the response cache on, all three read identically — Postgres is for multiple instances or SQL access, not for speed. And it stays a document model: one entry per row, no table per collection, no foreign keys. That keeps the way back to files open.

Want to see this on your own project?

Tell me briefly what you're planning. I'll show the admin UI on a real installation and say honestly whether it fits.

Request early access