Mirror, don’t migrate
When you need to evolve substrate, prefer adding a runtime mirror that reads from the canonical source and exposes a typed surface, over destructively replacing the canonical source with a new schema.
The mirror pattern keeps the old source authoritative through the transition, lets new consumers read from the typed runtime surface, and defers the cutover decision until evidence supports it.
What “mirror, don’t migrate” looks like
- Phase 0: identify the canonical source (a JSON file, an env var, an existing table, a third-party API). Don’t touch it.
- Phase 1: add a runtime model (a Prisma row, a derived API endpoint,
a typed in-memory cache) whose only job is to reflect the canonical source.
Updates flow
source → mirror. Reads happen against the mirror. - Phase 2: migrate consumers one at a time to read from the mirror. The canonical source stays authoritative throughout.
- Phase 3 (optional, often never): once every consumer reads from the mirror and writes against it, retire the canonical source on a deliberate cutover, not as a side effect of the new code landing.
Why this exists
Two concrete examples from the platform’s recent history:
- Governed-upgrade Phase 1 (PR #1076):
version.jsonis the canonical source for the installed bundle. Phase 1 added aPlatformConfig.bundleShamirror updated on platform boot, exposed/api/platform/version, and surfaced a UI surface — without touchingversion.json’s role as the source of truth on disk. A future Phase 2 can move to DB-authoritative, but not until the mirror has shown it can serve every consumer. - Reduction Gear Architecture (PR #1075):
GearInterfaceis proposed as a dual-emit envelope alongside existing event tables (ToolExecution,BuildPhaseRun,RuntimeVerification), NOT as a replacement. The existing tables remain authoritative for their owning surfaces; GearInterface becomes the common operator timeline only after emitters prove correctness, retention, and Cockpit usability.
The migrate-destructively alternative — drop the old table, point all consumers at the new one in one PR — produces three predictable failure modes:
- Lost evidence. Operator data living in the old table is gone the moment the migration drops it. Recovery costs hours.
- Atomic-PR blast radius. Every consumer must change in the same PR, so review takes longer, rollback is harder, and one missed call site breaks the platform.
- No graceful fallback. If the new schema turns out to need a column that wasn’t anticipated, you have to migrate again on top of a half-functional substrate.
When mirroring is the wrong default
- Bug-fix migrations. If the canonical source has a defect that every consumer experiences, fix the source — don’t mirror the bug into a runtime layer.
- Tiny consumers (< 3 call sites). The mirror overhead isn’t worth it for a one-shot rewrite where the cutover is mechanical.
- Schema changes the framework requires. Prisma column renames, for
instance, are physical — there is no mirroring possible around them.
Pair with
schema-honesty-over-aspirational-namingso the name change doesn’t smuggle new meaning into the rename.
The contract
Before opening a PR that replaces an existing source of truth, ask:
- Who is the current canonical source? Name it.
- Why must I replace it rather than mirror it? Document the reason.
- What evidence do I have that the mirror is unworkable? A claim of “easier to just migrate” is not evidence.
- What’s the rollback path? If you can’t draw a one-line rollback for the destructive option, the mirror is the architecturally sound choice.
Anti-patterns
- “I’ll just drop the old table and rebuild from this new one” without evidence that every consumer is moved.
- A new schema named for the future state, populated by a destructive migration from the old one, with no fallback.
- “We’ll just deprecate the old API in this PR” — deprecation without a mirror means the deprecation is the cutover.
Related principles
schema-honesty-over-aspirational-naming— paired discipline; the mirror should be named for what it carries today, not the aspirational future shapeone-data-model— the architectural reason (“not two integrated”); this principle is the migration-time pattern that honors it through the transitionnever-wipe-db-for-code-fixes— destructive substrate change is the extreme case; this principle is the general posture
Spec references
- Reduction Gear Architecture spec — §3.1, §4 dual-emit strategy
- Founder kernel evolution discipline spec — §6.1 promotion record