Every database project starts with the same question: where does the data live? The conventional answers are a server process, a binary format, a set of memory-mapped pages, or a cloud endpoint. grexa-db answers differently: the data lives wherever your filesystem puts it, which means it lives everywhere your filesystem already goes, and the consequences of that choice are the interesting part.

The short version is that grexa-db is a flat-file database engine where records are individual files and join relationships are directories full of symlinks, and the reason that matters is that every tool already written to read files from disk becomes, without modification, a query interface for your data: rg searches records, find traverses relationships, editors open rows, and your file-manager navigation habits become your database navigation habits, all without anyone involved having to know a database is present, though you do need rg --follow or find -L to traverse the join symlinks since most tools skip symlinks by default.

Relational databases model joins by matching foreign keys, which is a lookup operation that is O(n) for a nested-loop join, O(n log m) for an indexed nested-loop, or O(n+m) for a hash join, but in grexa-db the join is the filesystem: if an orders record belongs to a customer, that relationship is a symlink at customers/<customer-id>/orders/<order-id> pointing to ../../orders/<order-id> or a relative reference, and traversal of that link requires no userspace index lookup, just kernel path resolution on the same syscall that opened the directory, and it is fast because the kernel has already cached the directory entries it recently read.

This means traversal from a customer to their orders is ls customers/cust_123/orders/, and the result is every order file for that customer, with no query language, no index lookup, no connection string, just the shell; the same ls that lists your home directory lists your customer records, and the same cat that reads your /etc/hosts reads your order row.

The write cost is real: creating a new order requires creating the order file and then creating the symlink inside the customer’s orders directory, which means two syscalls with inode allocation and fsync on each, plus journal overhead if the filesystem has one, and a relational engine writing a row plus two index entries will batch those into fewer syscalls through its write-ahead log, which means grexa-db pays the per-call overhead twice per write, and if your workload is write-heavy that cost compounds fast; for read-heavy workloads where the data also needs to be human-inspectable and survivable with nothing more than rsync, the per-write overhead is the price we find acceptable.

Referential integrity and the deletion problem

The elephant in the room for grexa-db is referential integrity: if you delete an order file, the symlink in customers/<id>/orders/ does not disappear, it becomes a dangling symlink that ls will happily enumerate and cat will report as missing, and there is nothing in the base design that cleans this up, which means deletes must be a two-phase operation, remove the symlink first and the record file second, and if you are using grexa-db for anything where stale references are worse than missing records you need to implement that cleanup in the delete path before you ship, not after.

The join model handles the one-to-many case naturally and correctly, but many-to-many relationships require an explicit join-table directory, a third filesystem object that holds the symlinks from both sides, so a student enrolled in a class has students/<student-id>/enrollments/<enrollment-id> pointing to ../../enrollments/<enrollment-id> and simultaneously courses/<course-id>/enrollments/<enrollment-id> pointing to the same enrollment file, and the enrollment record itself holds the foreign keys that make it meaningful; the extra layer is where most of the complexity lives in a relational schema, and grexa-db does not make that complexity disappear, it makes it visible as a deeper directory tree and a more careful deletion protocol.

What you gain by being a directory

The thing nobody talks about with traditional databases is how opaque they are once the data is inside: you need a client, you need credentials, you need the right query syntax, and the data itself is encoded in whatever format the engine decided was efficient, which is to say unreadable to anything that does not speak that engine’s dialect; grexa-db’s data is readable by everything that speaks “file,” which is everything, and that has a compounding effect on the tooling around the database.

ripgrep searches every record without a WHERE clause, find gives you relationships, git tracks every change to every row because every row is a file, rsync backups are trivial and verified, and your IDE’s “find in files” works on live database content; the database that falls out of this is one where the operational tooling is the operational tooling for a regular directory tree, and the tradeoff is that predicate queries like “find all orders over $1000” require a full directory scan with rg, and if your access patterns are dominated by such filters rather than relationship traversal, grexa-db will feel slow fast.

The honest comparison to SQLite

We used SQLite for this before grexa-db existed, and SQLite is excellent: it is a proper B-tree with transactions and constraints and a query language that the whole world knows, and none of that is the problem; the problem was that SQLite’s data is in a file but the file is not the data in any meaningful sense, you cannot open a .db3 in an editor, you cannot grep it, you cannot diff two snapshots without the sqlite3 CLI, and you cannot back it up with anything other than sqlite3 .dump piped somewhere; grexa-db fixes the opacity problem by being fundamentally less capable but visibly so, which turns out to be the right tradeoff for the class of problem it solves, and the class of problem it solves is the one where your access patterns are relationship-first, your writes are bounded, and your team would rather rsync a directory than run a migration.