Storage
tCAD's two storage backends behind one domain contract: the JSON file backend, SQL via tDB or oxmysql, the atomic write path, the first-boot seed, and how to back the data up.
tCAD has two storage backends and one domain contract. Nothing above that layer mentions SQL, and nothing inside it accepts a request, a header or a cookie.
| Backend | Needs | Suitable for |
|---|---|---|
| JSON | Nothing at all | A small server, a test server, or a server with no database |
| SQL | tDB (recommended — no MySQL server required) or oxmysql | Anything larger |
Which one you get is decided by Config.storage; see Configuration.
They are substitutable, and that is measured
The contract file is normative, both adapters implement it, and a completeness check runs at boot: a missing operation is named at startup, by name, rather than turning up as a nil call mid-shift.
Both adapters pass the same 35-assertion self-test, 35 of 35, in a real FXServer — the JSON one against a real data.json, the SQL one against real SQLite through tDB. That is the substitutability claim demonstrated rather than asserted. Getting there took five rounds of execution finding what reading the code could not, including one worth naming because of its shape: two read operations were SELECT *, which cannot alias, so every call field came back in the database's column case where the UI reads another. No error, no exception — just a board rendering blanks on one backend and not the other.
The JSON backend
One file, data.json, in the resource folder, plus a data.json.bak. It holds everything — agencies, departments, calls, units, records.
It is gitignored and never shipped. A shipped data.json would silently overwrite a server owner's data on update.
The write path
Every write is: write a temporary file → fsync → rename over the target, with the previous version rotated to .bak. rename within a filesystem is the atomic primitive, so a reader never sees a half-written document.
Prove it on your own server:
tcad_writeprobe
in the server console. It reports whether a write actually landed, independently of whether anything else looked healthy.
Recovery
If data.json is unreadable or truncated at startup, tCAD recovers from data.json.bak rather than starting empty. Starting empty on a corrupt file would reseed over a server's history.
The one thing that cannot be proven from inside a process is crash consistency: demonstrating atomicity under power loss needs a crash, not a test. The .bak rotation is therefore kept as the recovery path rather than replaced by confidence in the rename.
Backing it up
# With the server stopped, or at least between writes.
cp data.json data.json.$(date +%F)data.json and data.json.bak are the whole of your data on this backend. Two things to know:
- They are in the resource folder, so a deployment process that replaces the resource folder wholesale deletes them. They are
escrow_ignore-adjacent in intent but not in mechanism: nothing in the update path protects a file that is not in the release archive, because it is not in the release archive at all. - A copy taken mid-write is a copy of the previous version, not a torn one — that is what the temp-file-and-rename scheme buys. It does not mean a copy is a consistent snapshot across several writes.
The SQL backend
Fourteen tables, created on first boot. The DDL is deliberately inside the intersection of the tDB and oxmysql dialects: no RETURNING, no DATE_ADD/INTERVAL, no GROUP_CONCAT … SEPARATOR, and explicit collation. A statement one backend accepts and the other refuses is the failure that lands on a customer rather than on a developer.
started is not ready
A database resource becomes able to answer asynchronously. Measured: tDB read as started about 2.5 seconds before it could serve a query. So tCAD probes a real export and waits, and until a backend is committed every operation answers starting.
The repair that measurement produced is worth stating because its absence was worse than the race: a latch on "no database yet" turned a 2.5-second startup race into a permanently unavailable schema. Schema creation therefore retries while the host is absent and latches only on a statement the database actually rejected.
Which resource is serving it
tcad_sqlprobe
reports which export names resolve and through which resource. That matters because provide 'oxmysql' satisfies a dependency and sets the resource state, and it does not alias the export namespace — measured, all 22 export names resolve via tDB's own namespace and none via oxmysql's. tCAD resolves the host resource name at runtime for that reason.
Backing it up
Use your database's own tooling. tCAD has no export or dump command of its own, and a CAD-shaped export would be a second format to keep correct.
The first-boot seed
Config.seed decides which world is created, and both backends seed the same world from one shared table. Seeding happens only into empty tables, so changing the value later does nothing — a staff member's edit to a label survives every restart.
tcad_reseed
re-runs it. See Configuration for what each value creates and why 'full' is the default.
Switching backends
There is no migration path between the two. The JSON document and the SQL schema hold the same domain, and nothing converts one into the other.
So a backend choice is effectively made once, at install. Switching means starting from a fresh seed, with the previous data readable only through whatever tooling that backend has. If you expect to need SQL, install tDB before you have data worth keeping.
Was this page helpful?
TIMMYG Studios