All Stories
Platform

Edit Live. Publish Clean. Roll Back Instantly.

A look inside the versioning engine that lets a builder reshape a live app while end users stay on a stable version, down to evolving the database without interrupting anyone mid-session.

The Design Goal

Two states for one app, at the same time

The whole point of GenieForge is that you build an app by talking to an agent that edits it live: it writes a tool, rewrites a page, runs a database migration, all in the flow, against your real data. That makes building fast and direct, and it means an app is always changing.

So the platform needs to hold two states of one app at once: the draft the builder is actively shaping, and the stable version end users, public API consumers, and scheduled jobs run against. The builder should feel zero friction, and everyone downstream should only ever see a version that was deliberately shipped.

That is a versioning problem with real depth. It spans the app blueprint (tools, pages, roles, prompts, automations) and the project databases underneath them, and it has to make moving between versions safe, reviewable, and reversible. Here is how the engine is built.

The Split

Keep building live, gate everyone else

The instinct was to move building into a separate staging environment. We rejected that: the best place to build is inside the project that has the data, and pushing builders away from real data cuts against everything the platform is for.

So we split by who is looking, not by where you work. The builder keeps editing the live tables, the draft, completely untouched. End users, public API consumers, and scheduled jobs are served a frozen published snapshot instead.

  • Build mode reads and writes the live draft, unchanged. Zero new ritual for the core loop.
  • Publish captures a snapshot and points end users at it. It is a deliberate act.
  • Roll back is a pointer flip back to an earlier snapshot. It is instant and it never disturbs the draft, so the builder keeps working.
  • Apps that predate publishing fall back to live, so nothing regresses until someone publishes for the first time.

Now a builder can tell the agent “rewrite this whole flow” on a Tuesday afternoon while customers are mid-session, and nobody downstream notices until they click Publish.

A Complete Gate

Every resource has to pass through one seam

A publish gate is only meaningful if everything passes through it. An app is a large surface, dozens of resource types, so we mapped each one against three precise questions: is it captured in a version, is it restored on rollback, and is it read from the published version at runtime? Every resource has a deliberate answer.

Captured for rollback. Custom roles, the settings schema, and per-model access rules are versioned alongside tools and pages, and restored in dependency order: roles are recreated before anything that references them, so the access model is always internally consistent after a rollback.

Deliberately excluded. Operational and security records (verified emails, OAuth links, collaborator grants) are intentionally kept out of versioning. They describe who can reach the app, not what the app is, so they persist across a rollback instead of time-traveling with the blueprint.

Resolved from the published version. The agent profile an end user picks, cron job prompts, event-hook wiring, and notification templates all resolve through the published snapshot at runtime, never the draft. And to make the gate legible, the Versions page renders a structural diff that shows exactly what the next publish will ship, collapsed to the net change per item.

The Hard Part

The database does not roll back with a pointer

Blueprint changes flip with a pointer. Schema changes do not. When you publish, pending migrations have to run against every project's real database, and a database change is physical and one-directional in a way a pointer is not. This is the genuinely hard part of versioning, and it drove the most careful design.

The naive approach (apply the whole migration batch, then flip the pointer) has a subtle race. For the moment between applying a drop and flipping, the still-live old version would be running against a database that has already lost a column. Any window where running code and live schema disagree is a window we refuse to ship.

There is a second, quieter hazard: adding a required column with no default. On a table that already holds rows the change fails outright, and even when it succeeds the currently published code keeps inserting without that column and gets rejected. Safe schema evolution has to account for both directions of compatibility.

Two-Pass Publish

Add before the flip, remove after it

The insight is that a publish carries two kinds of change. Some both the old and new versions tolerate: adding a table or a column, which the old version simply ignores. Some only the new version needs, and the old version must never see early: removing a table, column, or index that nothing in the new version references.

So we run publish in two passes around the moment the new version goes live:

  1. Apply everything except a trailing “remove only” batch. The old version still serves against these and tolerates them.
  2. Flip the pointer. The new version is now live, and it never names the things still queued for removal.
  3. Apply the removals. The new version does not use them and the old version has stopped serving, so this is the safe moment to drop.

No running version ever sees a schema it cannot handle. The design is deliberately conservative: it only defers removals that sit at the very end of the batch, so the order of changes is provably unchanged. Anything riskier (a rename, a type change, a drop followed by a re-add) falls back to the original single-pass behavior rather than guessing.

Guardrails

Steer toward the safe path, and a net under it

  • Required columns need a default. Adding a NOT NULL column without one is rejected, with guidance to set a default or use expand and contract: add it nullable, backfill, then tighten it in a follow-up.
  • Renames get a heads-up. A rename can briefly break code that still uses the old name, so the agent is nudged toward add, backfill, switch, drop.
  • Risky work gets a sandbox. Destructive changes against a project with real data are nudged into a throwaway draft fork: a clone of real-shaped data you can experiment in and discard on publish.
  • A backup under the whole thing. Before a risky publish we snapshot every project database. If the removal pass fails, we un-flip the pointer first and then restore, so a failed publish is never left half-applied.
The Result

Safe evolution, end to end

The result is a versioning engine that holds a live draft and a stable published version at the same time. A builder reshapes the app freely, publishes when ready, and can roll back with a single click, with the draft left untouched the entire time. Roles, settings, agent profiles, cron prompts, and templates all move through the same deliberate gate.

And the deepest part, evolving the database, is just another publish. A cleanup that drops a column can ship while customers are mid-session and nobody hits a missing field. That is the standard we hold the platform to: change should be fast for the builder and invisible to everyone else, all the way down to the schema.