The database is a projection of a markdown file
Why the source of truth for everything my command center does is a git-tracked text file — and the database is just a view I can throw away and rebuild.
By Andrew Pyle
Most systems get this backwards. The database is the truth, and any file — a config, an export, a README — is a second-class copy that drifts out of date the moment someone edits a row. My command center runs the other way around. The truth is a markdown file in git. The database is a projection of that file: something I can drop entirely and rebuild from the text, and nothing of value is lost.
That inversion sounds like a small implementation detail. It turned out to be one of the most load-bearing decisions in the system, and the one I'd port to any project I build next. Here's what it means, why it earns its keep, and where it strains when I'm not disciplined about it.
01Plans in git
Plans live in git, not in a dashboard.
The command center that plans and ships the projects across my portfolio has a rule I state plainly:
The source of truth is a markdown file; the database is a projection of it. Roadmaps and initiatives live in git — reviewable, diffable, portable across machines — not trapped in a dashboard's UI.
An initiative — a scoped piece of work with phases, goals, and constraints — is not a record I create in a web form. It's a committed file like tasks/initiative_0221_portfolio_seo_consolidation.md: the header carries its status, owner, and size; the body carries the phases. Something like this lives in the repo:
# Initiative 0221 — Portfolio SEO consolidation
**Status**: in_progress
**Owner**: AACC
**Goal**: engine_agentic
**Effort**: xl
**Complexity**: high
## Phase A — finish the content pipeline guardrails
## Phase B — audit cannibalization per site
## Phase F — recovery gate: measure before scalingA parser reads that file and upserts the rows the web portal renders. The portal is where I look at the work, never where it's defined. The proof that the file is primary is in the update path: to change a plan I edit the markdown and push it again, and the parser PATCHes the matching rows, phase by phase, in order. There is no "save" in the portal that writes back to the text. Drop the database, re-run the parser over the files, and the portal comes back identical. The file is the record; the row is a convenience. It's the same stance I take with a live server — the box is a copy the deploy installs, never the place the truth is authored.
02Review for intent
I get code review on my plans.
The payoff is not aesthetic. It's that all the machinery I already trust for code now applies to my intentions.
Diffs and history, for free
When a plan changes, I don't get a mystery — I get a diff. Who moved the recovery gate to the end? When did phase C get added? The answer is in git log, the same place I'd look for a code change. My planning has an audit trail because it lives where audit trails already exist. A dashboard shows the current state; the file shows every state it passed through to reach it.
Review before it's real
A change to an initiative can go through a pull request. I can stress-test a plan, leave comments on it, and merge it deliberately — the same gate I put in front of code that touches production. That matters because I plan adversarially: before a big build starts, the plan gets poked at for the ways it repeats a past mistake or over-builds. Putting a plan in git is what makes that scrutiny mechanical instead of optional.
Portable across every machine
I work across more than one Mac, and a fleet of agents runs against the same repo. Because the truth is a file in git, every machine sees the same state after a pull. There's no "which dashboard has the latest" problem, because the dashboard was never the thing holding state. The repo is. If the portal and a file ever disagreed, the file wins, and a re-sync makes it so.
A roadmap trapped in a hosted tool's database has none of this — you can't diff it, you can't branch it, and you certainly can't rebuild it on another laptop with a git pull. The plan and the code sit in the same substrate, move through the same review, and travel together.
03The honest line
The projection has to be honest.
This pattern isn't free. It only works if you hold two lines.
First, there's a parser contract. The markdown can't be freeform if a machine has to read it — fields have shape, phase identifiers have constraints, and a malformed file should fail loudly rather than sync half a plan. That's a real cost, and I've paid it: a phase slug is stored in a varchar(64) column, so a phase title that reads fine to a human but runs past sixty-four characters once slugified quietly breaks the sync. The fix isn't to loosen the database — it's to validate the file against the contract before it's ever pushed, so the failure lands at author time, in a diff, not at sync time in production.
Second, and non-negotiably, the projection must stay rebuildable. The instant a database row holds something that isn't derivable from the files — a status somebody clicked in the UI that never made it back to the markdown — the inversion is broken, and the database has quietly become a second source of truth. The whole value depends on being able to say, at any moment: drop the DB, re-run the parser, lose nothing. If that stops being true, you don't have a projection anymore. You have drift. It's why the portal has no write-back path — the moment one exists, the file stops being the truth and becomes a stale mirror of it.
# the invariant that has to hold, always:
$ drop_database()
$ parse("tasks/*.md") --> upsert_rows()
# portal is byte-for-byte what it was. nothing lost.The reason I care beyond my own convenience is that it's a small model of a bigger belief: the durable thing should be the one you can read, review, and carry — plain text under version control — and the database and the dashboard on top of it should be disposable. Build it in that order and your plans get the same rigor as your code. Build it the other way, and one day you're afraid to touch the database because it's the only place the truth lives.
04Where it strains
Where the model strains.
I want to be honest about the seams. Files-as-truth has one that bit me repeatedly, and it comes from the same place its strength does: the file, not the database, decides what's real.
Every initiative gets a number, and the number is part of the filename. To pick the next free number, a helper scans the files and tells me the lowest one open. That's fine with one person at one keyboard. It stops being fine when eight agent sessions run at once, because the lookup is a snapshot, not a reservation. Two sessions minutes apart can both be told 0231 is free, both write initiative_0231, and now two plans claim one identity. The database would have stopped that with a unique constraint; the filesystem shrugged.
The thing that makes files-as-truth portable — that any machine can write one without asking a central server for permission — is the same thing that lets two machines write the same one. The strength and the sharp edge are one property seen from two sides.
The fix isn't to move truth back into the database. It's to make the write path aware of the concurrency the model invites: re-verify the number immediately before pushing, and again if the branch sat in CI while other sessions moved. I isolate file-mutating agents in their own git worktrees so their parallel writes don't collide — but numbering is a shared namespace no worktree fences off. The lesson I keep relearning is that choosing files over a database doesn't delete the problems a database solved; it moves them into the write path, where I have to solve them on purpose.
05The pattern repeats
The same inversion, everywhere I look.
Once you build one system this way, you see your setup was already shaped like it. My agents' long-term memory is a set of markdown files — one fact per note, categorized, hand-curated. The index an agent reads at the start of a session is a projection of those notes, not a separate store I keep in sync. A capability guide telling agents what the command center can do isn't hand-written either — it's regenerated from the code, so it can't drift from what's true. Even the sitemap I hand search engines is built from the same source of truth the site renders from, so it never lists a page I've quietly retired. In every one, the readable artifact is the truth and the machine-friendly form is downstream — regenerated, never authored.
That's the through-line. "Done" is a claim; the file is the fact — and the systems I trust won't let a status be true in a database until it's true in something I can read and carry. The database earns its place as a fast, queryable view of that truth — never the truth itself. Get the direction right and the whole thing becomes calm: I can drop the fast layer and rebuild it any afternoon, because the part that would hurt to lose was never in there.