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.