Deploy ist eine Branch-Promotion, keine Migration
Der übliche Weg in die Produktion verschiebt Daten. Du ziehst einen Snapshot von dev, spielst ihn in eine separate Prod-Datenbank zurück, lässt dort die Migrationen laufen und hoffst, dass die beiden Schemas zwischenzeitlich nicht auseinandergedriftet sind. Jetzt betreibst du zwei Datenbanken, die gleich sein sollen und es langsam nicht mehr sind — und der Kopierschritt wird träger, je mehr deine Daten anwachsen.
Wenn deine Datenbanken Copy-on-Write-Timelines auf einem gemeinsamen Page-Store sind, hat ein Deploy eine völlig andere Form. Produktion ist keine separate Datenbank, in die du hineinkopierst — sie ist ein Branch. Live-Gehen ist eine Promotion: eine Metadaten-Operation, die einen Pointer verschiebt, keine Bytes. Dieser Beitrag ist das Modell, um das herum die Timeline-Architektur der Plattform gebaut ist — und die Mechanik darunter.
Er baut auf zwei Primitiven auf, die anderswo behandelt werden: Copy-on-Write-Branching an einer LSN und Scale-to-Zero-Compute. Falls dir das neu ist, überflieg sie zuerst — dieser Beitrag setzt sie zusammen.
Das Modell: Prod ist ein dauerhafter Branch, geforkt von dev
Die Datenbank eines Projekts ist eine Timeline. Im Alltag ist diese Timeline die dev-Umgebung — hier baut und iteriert der Agent. Die Produktion ist eine zweite Timeline: ein dauerhafter Branch, auf den der Traffic der Nutzer trifft.
Der erste Deploy forkt den aktuellen Head von dev in eine neue, dauerhafte Prod-Timeline. Ab da ist Prod seine eigene Timeline mit den Daten echter Nutzer, und dev läuft unberührt auf seiner eigenen Timeline weiter. Weil Branching O(1) Copy-on-Write ist, kopiert der Fork keine Daten — ein Deploy kostet gleich viel, ob die Datenbank ein Kilobyte oder hundert Gigabyte hält.
Zwei Konsequenzen folgen direkt aus „Prod und dev sind dieselbe Engine, einen Branch auseinander":
- Kein Provisioning-Drift. Prod ist keine separat hochgezogene Datenbank, in die du per Dump-and-Restore hineingehst — es ist ein Fork, der beim Split das Migrations-Ledger von dev geerbt hat. Ein Deploy wendet nur die wirklich ausstehenden Migrationen an (das DDL, das dev ausgeführt hat und Prod noch nicht), also gibt es keine „läuft auf dev, bricht auf Prod"-Lücke durch zwei unabhängig provisionierte und migrierte Datenbanken.
- Preview gegen echte Produktionsdaten. Du musst ein Produktionsproblem debuggen oder eine Änderung gegen Daten in Prod-Größe testen? Fork Prod in eine Wegwerf-Timeline und stochere darin herum — Copy-on-Write, isoliert, weggeworfen, wenn du fertig bist. Die echte Prod-Timeline merkt nichts davon.
Die Mechanik der Promotion
Die erste Promotion ist ein einziger Metadaten-Schritt: den Head von dev in eine dauerhafte Prod-Timeline forken. Die Entscheidung ist rein — anhand der aktuellen Branches, was zu Prod wird — und die Ausführung ist eine Branch-Operation ohne Datenkopie:
Die Promotion-Logik ist eine kleine reine Funktion — planPromotion —, die auf die Branches des Projekts schaut und entscheidet, welche Timeline zu Prod wird (und ein No-op ist, wenn dev bereits gleich Prod ist). Die Ausführungsschicht forkt den Head von dev, verzeichnet den neuen Prod-Branch, markiert ihn als den prod ausliefernden Branch und provisioniert ein darauf gerichtetes Prod-Compute. Ein schlechtes Deploy zurückzurollen ist dasselbe Branch-an-einer-LSN-Primitiv wie Time-Travel, angewandt auf die Prod-Timeline: einen früheren Punkt wählen, forken, umbiegen — kein Restore-from-Backup.
Das war die erste Promotion. Ein Re-Deploy forkt nicht neu: er behält die laufende Prod-Timeline — die Daten echter Nutzer bleiben über Releases hinweg erhalten — und schiebt sie vor. Er pinnt den frisch releasten Commit und wendet alle ausstehenden db/migrations als forward-only DDL auf Prod an (runPendingMigrations({ env: "prod" })), nachdem er einen LSN-Checkpoint vor dem Deploy als Rollback-Anker gesetzt hat. Ein Deploy führt also sehr wohl Schema-Migrationen gegen Prod aus; was er nie tut, ist Daten kopieren. Der einzige Weg, der Prod-Daten mit denen von dev überschreibt, ist ein expliziter, separat gesnapshotteter Reset — niemals ein Seiteneffekt des Ausrollens.
Getrenntes Compute pro Umgebung — und beide skalieren auf null
Prod und dev müssen gleichzeitig live sein: Kunden treffen auf Prod, während der Agent auf dev weiter iteriert. Ein einzelner Postgres-Prozess ist beim Start an eine Timeline gebunden, also bekommen die Umgebungen getrennte Computes — neon-compute für dev, neon-compute-prod für Prod — jedes auf seine eigene Timeline gerichtet.
Das klingt nach doppelten Kosten, und das wäre es auch, würden nicht beide Computes unabhängig auf null skalieren (der Scale-to-Zero-Proxy routet pro Umgebung nach Datenbanknamen und weckt jedes bei Bedarf):
- App ohne Besucher und niemand baut → beide auf null.
- Ein Kunde kommt vorbei → Prod wacht auf; dev bleibt auf null.
- Der Agent iteriert → dev wacht auf; Prod bleibt auf null.
Du zahlst pro zugegriffener Umgebung, nicht pro provisionierter. Die Manifeste werden pro Umgebung aus einer einzigen Vorlage generiert (generateNeonComputeManifests(env)), ein Prod-Compute ist also dasselbe Ressourcen-Set wie dev, nur mit eigenem Namen und eigener Timeline.
Die Materialisierung ist lazy: ein Projekt, das noch nie deployt wurde, hat genau ein Compute (dev). Die Prod-Timeline und ihr Compute entstehen erst bei der ersten Promotion — der lange Schwanz nie deployter Experimente trägt also überhaupt keinen Prod-Overhead.
Was dir das bringt
- Deploy ist sofort und datenmengen-unabhängig. Es ist ein Fork plus ein Pointer-Move, kein Dump/Restore. Eine 50-GB-Datenbank wird genauso schnell promotet wie eine leere.
- Kein Provisioning-Drift. Dieselbe Engine, einen Branch auseinander — Prod wurde von dev geforkt und hat dessen Migrations-Ledger geerbt, ein Deploy wendet also nur das ausstehende DDL an, nie einen blinden Full-Re-Run gegen eine separat gebaute Datenbank.
- Preview auf echten Daten. Fork Prod in eine Wegwerf-Timeline, teste gegen Daten in Produktionsgröße, wirf sie weg. Die Produktion bleibt unberührt.
- Rollback ist point-in-time. Prod ist eine Timeline, das Zurücknehmen eines schlechten Deploys ist also dieselbe Branch-an-einer-LSN-Operation wie Code-und-Daten-Time-Travel — angewandt auf Prod.
- Leerlauf ist kostenlos, pro Umgebung. Scale-to-Zero auf jedem Compute heißt, die zweite Umgebung ist kein stehender Kostenfaktor; sie ist da, wenn darauf zugegriffen wird, und weg, wenn nicht.
Die Form der Sache
Hör auf, dir die Produktion als einen Ort vorzustellen, an den du deine Datenbank kopierst, und fang an, sie als einen Branch zu begreifen, den du promotest. Auf Postgres mit getrenntem Storage und Compute hört „Deploy" auf, ein Datenmigrations-Ereignis mit Kopierschritt und Drift-Risiko zu sein, und wird zu dem, was es sein sollte: einen Pointer verschieben, der sagt „diese Version bekommen die Kunden". Die Daten waren schon da — geteilt, Copy-on-Write, einen Branch von jeder Umgebung entfernt.