Deploy is a branch promotion, not a migration
The usual path to production moves data. You snapshot dev, restore it into a separate production database, run the migrations there, and hope the two schemas haven't drifted in the meantime. You now operate two databases that are supposed to be the same and slowly aren't, and the copy step gets slower as your data grows.
If your databases are copy-on-write timelines on a shared page store, deploy has a different shape entirely. Production isn't a separate database you copy into — it's a branch. Going live is a promotion: a metadata operation that moves a pointer, not bytes. This post is the model the platform's timeline architecture is built around, and the mechanics underneath it.
It builds on two primitives covered elsewhere: copy-on-write branching at an LSN and scale-to-zero compute. If those are new, skim them first — this post composes them.
The model: prod is a durable branch forked from dev
A project's database is a timeline. Day to day that timeline is the dev environment — where the agent builds and iterates. Production is a second timeline: a durable branch that customer traffic hits.
The first deploy forks dev's current head into a new, durable prod timeline. From that point prod is its own timeline carrying real users' data, and dev keeps running on its own timeline, untouched. Because branching is O(1) copy-on-write, the fork copies no data — a deploy is the same cost whether the database holds a kilobyte or a hundred gigabytes.
Two consequences fall straight out of "prod and dev are the same engine, one branch apart":
- No provisioning drift. Prod isn't a separately stood-up database you dump-and-restore into — it's a fork that inherited dev's migration ledger at the split. A deploy applies only the genuinely-pending migrations (the DDL dev ran that prod hasn't), so there's no "works on dev, breaks on prod" gap from two databases provisioned and migrated independently.
- Preview against real production data. Need to debug a production issue or test a change against prod-scale data? Fork prod into a throwaway timeline and poke at it — copy-on-write, isolated, discarded when you're done. The real prod timeline never feels it.
The promotion mechanics
The first promotion is a single metadata move: fork the dev head into a durable prod timeline. The decision is pure — given the current branches, what becomes prod — and the execution is a branch operation with no data copy:
The promotion logic is a small pure function — planPromotion — that looks at
the project's branches and decides which timeline becomes prod (and is a no-op if
dev already equals prod). The execution layer forks the dev head, records the new
prod branch, marks it serving prod, and provisions a prod compute pointed at it. Rolling back a bad
deploy is the same branch-at-LSN primitive as time-travel, applied to the prod
timeline: pick an earlier point, fork, repoint — no restore-from-backup.
That's the first promotion. A re-deploy doesn't re-fork: it keeps the live
prod timeline — real users' data persists across releases — and advances it. It
pins the newly-released commit and applies any pending db/migrations to prod as
forward-only DDL (runPendingMigrations({ env: "prod" })), after tagging a
pre-deploy LSN checkpoint as a rollback anchor. So a deploy does run schema
migrations against prod; what it never does is copy data. The only path that
overwrites prod data with dev's is an explicit, separately-snapshotted reset —
never a side effect of shipping.
Separate compute per environment — and both scale to zero
Prod and dev have to be live at the same time: customers hit prod while the
agent keeps iterating on dev. A single Postgres process is bound to one timeline
at boot, so the environments get separate computes — neon-compute for dev,
neon-compute-prod for prod — each pointed at its own timeline.
That sounds like double the cost, and it would be, except both computes scale to zero independently (the scale-to-zero proxy routes per-environment by database name and wakes each on demand):
- App with no visitors and nobody building → both at zero.
- A customer visits → prod wakes; dev stays at zero.
- The agent iterates → dev wakes; prod stays at zero.
You pay per environment accessed, not per environment provisioned. The
manifests are generated per environment from a single template
(generateNeonComputeManifests(env)), so a prod compute is the same resource set
as dev with a distinct name and timeline.
The materialization is lazy: a project that hasn't deployed has exactly one compute (dev). The prod timeline and its compute come into existence at the first promotion — so the long tail of never-deployed experiments carries no prod overhead at all.
What this gives you
- Deploy is instant and data-size-independent. It's a fork + a pointer move, not a dump/restore. A 50 GB database promotes as fast as an empty one.
- No provisioning drift. Same engine, one branch apart — prod was forked from dev and inherited its migration ledger, so a deploy applies only the pending DDL, never a blind full re-run against a separately-built database.
- Preview on real data. Fork prod to a throwaway timeline, test against production-scale data, discard. Production is untouched.
- Rollback is point-in-time. Prod is a timeline, so reverting a bad deploy is the same branch-at-LSN operation as code+data time-travel — applied to prod.
- Idle is free, per environment. Scale-to-zero on each compute means the second environment isn't a standing cost; it's there when accessed and gone when not.
The shape of it
Stop thinking of production as a place you copy your database to, and start thinking of it as a branch you promote. On separated storage/compute Postgres, "deploy" stops being a data-migration event with a copy step and a drift risk, and becomes what it should be: moving a pointer to say "this version is the one customers get." The data was already there — shared, copy-on-write, one branch away from every environment.