Field notes

Mechanism · Chapter 03

Two planes

Your data lives on your device. Your billing tier does not. Living with that split costs six files per table, and we have the scar to prove it.

Offline-first has an obvious follow-up question that took me embarrassingly long to ask properly.

If the client holds a real database and writes to it directly, what stops a client from writing whatever it likes?

Nothing does. That is the honest answer. A local-first architecture hands the user's machine genuine write authority, and a machine you do not control will eventually be asked to do something you would not have authorised.

Most of the time this is fine, because most of the data is theirs. If someone edits their own note badly, that is between them and their note. Sync carries it up, the server stores it, everyone's copy agrees, nobody is harmed.

Then there is the other kind of data.

The data that cannot go on the plane

A workspace's subscription tier. A member's role. Whether a person belongs to a workspace at all.

None of that can live on the offline-first plane, because none of it is a fact about the user's own content. It is a fact about their entitlements, and a client that can write its own tier is not a business. The same goes for roles: if the device can promote itself to owner, permissions are decoration.

So there are two data planes, with different rules.

One is offline-first: boards, cards, notes, habits, expenses, everything that is the user's own material. It lives in the browser database, it works on a plane, and the server is a synchroniser rather than an authority.

The other is server-authoritative: billing, roles, membership, entitlements. It never syncs to the client as writable state. You ask the server, the server decides, and the answer arrives over the network or not at all. These screens genuinely do not work offline, and that is correct rather than a limitation.

Getting this boundary right is, I think, the single most consequential architectural decision in the product. It is also invisible when done well, which is why it took me a while to realise how much of the design it drives.

The tax

Two planes means two ways of doing everything, and the cost lands on whoever adds a table.

A new synced table has to be declared in six places. The Postgres schema. The migration. The SQLite mirror on the client, where the type system is narrower and booleans are integers and JSON is text. The sync rules that decide which users receive which rows. The ordering list that respects foreign keys, so a child never arrives before its parent. And the write-authorisation layer, which decides whether an incoming change from a device is allowed at all.

Six places, and the failure mode is the worst kind. Miss one and nothing errors. The table is simply not there on the client, or arrives without its parent, or is refused on write, and the feature is quietly empty for exactly the users whose data it holds.

That happened to us with the seasonal scoring table for the family house cup. Worked in development. Present in production. Empty on every client. Five of six places done, and the missing one did what missing ones do, which is nothing at all, loudly.

There is a checklist now. The checklist exists because we needed it, which is the only reason any checklist should exist.

The part that is not a compromise

I want to be careful not to present this as a sad trade-off we grudgingly accept.

The split is what lets both halves be honest. Because the user's own data is genuinely local, we can promise it works on a plane and mean it. Because entitlements are genuinely server-side, we can enforce them without pretending a client-side check is security.

The alternative designs are worse in ways that only show up later. Put everything server-side and you have a fast website that is useless in a tunnel. Put everything client-side and you have a beautiful offline app whose billing can be edited with devtools.

Two planes is more work per table, permanently. In exchange, neither promise has to be quietly weakened later, and I have come to think that is what architecture is actually for: choosing which costs you would rather pay continuously, with your eyes open, rather than discovering them.