We reversed our biggest design decision, and it cost almost nothing
The design said git was the system of record. A change was a commit, a revision a commit id, a proposal a ref kept alive. It was written down, argued for, and — for a while — right.
We reversed it. Sqlite is the system of record now: an append-only changes log plus a
content-addressed blobs table, with everything else a derived index. This is the note
about why, and about the thing that made the reversal affordable.
The timing was the whole point
We reopened it the day before writing the git adapter. That is not luck; it is the last moment the question is free.
And it was nearly free by construction, because the ports were designed to keep storage out of the language above them. The repository port speaks resources, revisions and changes. A revision is an opaque non-blank string — deliberately not “a commit id”. Nine use-case slices, two API doors and a full contract sat on those ports, and not one of them would change.
What was not free: three sections of the design document described git mechanics directly — loose-object replication, the ref layout for proposals, the on-disk shape. Those had to be rewritten. That is the honest cost of a reversal at this stage: a day of prose, not a month of code.
What the store actually has to do
Before comparing anything we wrote the list, taken from the ports and the invariants rather than from either candidate. Anything on the list a candidate cannot do cheaply is a real objection. Anything not on it is not an argument.
- Append one change, write and record fused, idempotent by content.
- Read a path at a revision, including head.
- List changes newest first, as of a revision, optionally scoped to a path.
- Diff two revisions into changed resources with a kind.
- Make proposed state revision-addressable, so reviewing a proposal is the ordinary diff call.
- Give content a stable identity, because chunk ids are hashes of it.
- Survive an ephemeral disk, and a wake that stays routine rather than becoming an incident.
- Serialise writes — one team, one writer.
- Never be the query layer.
Note what is absent: branching, merging, rebasing, blame, divergent lines of work. We had already decided against merges four days earlier. Which means the model git is uniquely good at was one we had explicitly designed away — and we were still planning to pay for it.
What git bought, honestly
Snapshot semantics for free: a revision names a whole tree, so “read everything at R” and “diff A to B” are native rather than assembled. Content addressing and dedup out of the box. A history implementation somebody else already got right.
And a genuinely good product story — your context is a repo, cloneable, readable with tools everybody already has.
That last one is not nothing. If “your context is a git repo” were a promise rather than an implementation detail, or if bidirectional sync with a customer’s own repository were on the roadmap, git would still win. Neither is true for us.
What git cost
A second durable store. The tenant already runs sqlite, which already has replication and a restore path that is already tested. Git meant a second one of each — a second thing to get right, a second thing to be woken up by.
A cold start proportional to history. One fetch per object, on the wake path that requirement 7 says must stay routine. The fix is packfiles, which is go-git’s weakest area.
A lot of small objects. One commit per changed resource per connector sync, needing repacking eventually.
What sqlite bought
One storage engine, one replication mechanism, one restore, one boot phase. Writes transactional across content and metadata in the same commit.
And the queries git supposedly makes easy turned out easier. “What changed between A and B” is a range over sequence numbers. “The tree at R” is a latest-change-per-path window. A proposal is a set of rows with a parent revision, and “main versus proposal” is a join with no ref machinery in it at all.
The rule that came out of it
Making the log the truth creates one obvious hazard, so we wrote the rule down where it cannot be missed: never answer a query by walking the log. Every read is a point lookup or a small range. Search, ranking and navigation belong to the derived indexes, which are rebuildable and therefore allowed to be wrong in ways the record is not.
The transferable part
Not “use sqlite”. The transferable part is that the decision was cheap because the ports never let storage vocabulary leak upward. A revision being an opaque string rather than a commit id looks like pedantry when you write it. It is what turned a foundational reversal into a document edit.
Design the seam before you are sure which side of it you want.