Skip to content

One timeline per document

No lesson shaped datadata’s design more than this one. It comes from years of building systems with heavy schema evolution — a headless CMS and Shiftic: a document’s meaning should never be split across histories that move at different speeds.

It matters because data and the schema that gives it meaning only make sense read together. Let their histories drift onto separate timelines and the past gets harder to read — to say what a document meant when it was written, you have to reconstruct which schema was in force, rather than have the data carry that answer itself.

In a conventional stack, what a stored record means lives in at least three places: the data itself, the database schema, and the source code that reads and writes it. Each has its own history — the data’s, the migration log’s, the repository’s — and they are correlated only by deploy timestamps. Answering “what did this document mean when it was written?” becomes archaeology across git history, deploy logs, and migration tables.

Schema evolution widens the cracks. Database changes and code changes never land atomically — there is always a window where new code meets old shapes or old code meets new ones. And some changes leave no trace where the data lives at all: when the schema is defined in application code — a Zod schema, say — rather than in a migration table, remove a field and the only record of the removal is a commit.

With one central database, you can at least pretend a change happened at a single moment. In a distributed deployment there isn’t even that. datadata runs one isolated store per folder, and a rollout reaches each folder at its own moment, as instances restart. “When did the schema change?” has no global answer — only a per-folder one. Any design that assumes one deploy moment is telling a fiction somewhere.

Make the per-document event log the only timeline that matters. Three decisions follow directly:

  • Schemas are documents. Schema history lives in the same event log as the data it governs. Even TypeScript-authored schemas are upserted into documents at startup — so a code rollout becomes a document write in each folder, and each folder’s log records when the change reached it: the only answer an eventually-consistent fleet can give.
  • Changes are data, not domain events. A JSON Patch is self-applying. Domain events need a reducer to interpret them — and that reducer is a second timeline of its own: fix a bug in it and last year’s events quietly mean something new on replay. A patch has no interpreter to drift from, so history replays identically forever.
  • Every event is tagged with its schema version. Any historic version of any document is interpretable with the exact schema it conformed to — in place, or exported to somewhere that has never seen the source code.

What this buys: point-in-time introspection has exactly one place to look, and a folder’s history is portable because it carries its own interpretation with it.

You might expect a review-before-it-lands layer to work like a branch: fork the document, edit the copy, merge later. That spins up a divergent history you have to reconcile on the way back. Staged sessions do the opposite: staged changes ride as an overlay on the live document, which keeps advancing underneath them. The session tracks the moving head in real time and flags the moment an upstream change conflicts with staged work — it stays caught up with the timeline rather than drifting away from it. Commit lands the staged changes on the current head atomically, or discard drops them and nothing reaches the document. Either way there is only ever one committed timeline, with a provisional layer kept separate on top.