Ask a room of engineers where application data should live and you will get the same answer you would have gotten in 2005: put it in a database, pick Postgres if you are unsure, move on. The answer is not wrong, but it skips the interesting question, which is what your access pattern actually looks like once the feature ships, because a database is not a place to put data so much as a bet about how you will read it back, and three of our projects made three different bets that all landed on the same substrate: plain files on a filesystem, no server process, no wire protocol, no daemon between the data and the tools that read it.
SQLite, IcefallDB, and grexa-db are the three bets, and they are worth examining together because each one keeps a different promise, and the promises do not overlap as much as you would expect; SQLite promises transactional correctness in a single file, IcefallDB promises analytical scans over immutable columnar files with mutations layered on top, and grexa-db promises that the data is legible to every tool on the machine without a client library at all.
SQLite: the file is the database, but only barely
SQLite is the answer everyone reaches for first, and it earned that position honestly, since it gives you a real B-tree, real ACID transactions (a rollback journal by default, a write-ahead log when you enable the mode), real constraints, and a query language the entire industry already knows, all inside one .db3 file, which is why it keeps showing up as the embedded store in small apps everywhere. The catch is the one we wrote about in the grexa-db post: the file is the database in a literal sense, but it is not the data in any usable sense, because you cannot open a .db3 in an editor, you cannot grep it, you cannot diff two snapshots without shelling out to the sqlite3 CLI, and even backing it up is not the cp or rsync operation people assume, since copying a live database can tear a page mid-write and the safe paths are sqlite3 .dump, VACUUM INTO, or the online backup API; the operational tooling around a SQLite file is basically the sqlite3 CLI plus a short bench of helpers like sqldiff and Litestream, and everything else you build yourself.
That trade is correct more often than people admit, because the moment your access pattern includes ad-hoc predicates, concurrent-ish writes, or anything that looks like SELECT ... WHERE against a filter you did not anticipate at design time, SQLite is doing real work that a directory tree cannot, and the opacity of the file is the price of having a query optimizer standing between you and your own rows; we reach for it whenever the read pattern is relational first and human inspection is a nice-to-have rather than the point.
IcefallDB: plain files that an analytics engine can chew on
IcefallDB made a different bet, which is that the files should be Parquet, the metadata should be plain JSON, and the query engine should be DataFusion rather than anything we wrote ourselves, so the storage layer is a directory of standard Parquet files plus a manifest that records which row groups are live, and the consequence of that choice is that the data is not human-legible the way grexa-db’s is, but it is tool-legible in a different and arguably wider direction, because every analytics engine from DuckDB to Spark can open the Parquet files directly even if it knows nothing about IcefallDB’s manifest, and the escape hatch is always there when you need it.
The mutations and the aggregate sidecar sit on top of that immutable base as a thin pretension of mutability, with copy-on-write at the row-group level and a lazy .agg cache keyed by column, predicate hash, and row group, and the reason this design works is that the access pattern it serves is scan-heavy and append-mostly: analytical queries that sweep columns, batch writes that land in bursts, and almost no point lookups, which is nearly the opposite of the pattern SQLite serves, even though both are “just files” when you squint at the directory listing.
grexa-db: the filesystem is the query interface
grexa-db is the most radical of the three bets, and also the narrowest, because it promises that records are files and joins are directories of symlinks, which means the query interface is rg, find, ls, and your editor, and there is no engine at all between you and the rows; the bet pays off when your access pattern is relationship-first, when traversal from a customer to their orders is the common case and predicate filters are rare, when writes are bounded enough that paying two syscalls plus journal overhead per write is acceptable, and when the team would rather rsync a directory than run a migration, all of which we covered in the dedicated post and will not re-litigate here.
What is worth saying in this context is that grexa-db only makes sense as one corner of a triangle, not as a general answer, because the moment you need a filtered aggregate across every record you are back to a full directory scan with rg, and the moment you need multi-column indexing you have reinvented a database badly, so the design holds precisely because its scope is honest about the pattern it serves.
The decision is the access pattern, not the format
Laying the three next to each other, the framework falls out on its own: if your reads are ad-hoc relational queries with filters you cannot predict, SQLite, because the query optimizer is doing work you do not want to write; if your reads are column sweeps over large immutable history with occasional batch mutation, Parquet plus a manifest, because the format is already the lingua franca of every analytical engine; if your reads are relationship traversal by humans and shell tools, files and symlinks, because every client already exists; and if none of those descriptions fit, that is when you reach for Postgres or MongrelDB and accept a server process as the cost of having a general answer.
The mistake we see most often, in our own older code as much as anywhere, is picking the format before characterizing the reads, which is how you end up with a JSON-file store that needed indexes, or a SQLite database that only ever serves get_by_id and would have been happier as a directory, or a Postgres instance guarding 400 rows that change twice a year; the filesystem is the right database more often than the industry default admits, but only when the access pattern says so first, and the three projects above are what it looks like when you let the pattern pick the storage instead of the other way around.
