The databasePart 4 of 6

Giving the agent an undo button for your data

An AI agent that builds your app also runs migrations, bulk edits, and seeds — destructive operations that git can't undo, because git only versions code. So we gave the agent a data undo it wields itself: it snapshots before a risky operation and rolls back if the operation breaks the data. Here's how it works on copy-on-write Postgres, why restore keeps the same connection string, and why a fleet of these apps costs nothing while idle.

Giving the agent an undo button for your data

An AI agent that builds your app doesn't just write code — it runs migrations, seeds tables, and issues bulk UPDATEs. Code has a great undo: it's called git, and every generation is a commit. Data doesn't. An agent that runs a migration dropping the wrong column, or a bulk reprice with an off-by-one WHERE, has done something git can't touch. For a fast, autonomous agent, data is the unprotected surface.

We already gave the platform's build agent a commit-tied code+data undo (time-travel for code and data). This post is about the active half: handing the agent a snapshot/restore it wields as a tool, so it can protect itself during a run — snapshot before a risky operation, roll back if the operation breaks the data — and so you can ask, in plain language, to undo what happened. The trick that makes it affordable everywhere is the same one that lets these apps sleep for free.


Snapshots are markers, not copies

The naive way to make a destructive operation safe is "copy the rows first." On a normal database that's real storage and real time — absurd to do reflexively before every risky step, across thousands of small apps.

Adorable runs a self-hosted Neon: storage (the pageserver) is separated from compute (a stateless Postgres), and history is a timeline — the database reconstructed from versioned pages + WAL up to an LSN. Branching a timeline is copy-on-write and O(1), independent of database size. So a snapshot isn't a copy of your data; it's a two-field marker — (timeline_id, lsn) — that costs effectively nothing until you actually restore.

When a snapshot is a row, not a copy, the default can flip: snapshot before every risky operation, reflexively. That's exactly what the agent now does.

The agent brackets its own risky operations

The producer — the agent that generates and iterates your app — is the thing that runs the scary operations. So it's the thing that snapshots. Two paths:

  • Automatic, at the clearest boundary. apply_migrations takes a safety snapshot before it runs. A migration that drops a column or backfills wrong can't be undone by editing code — only by a data restore — so the restore point is captured without the agent having to remember.
  • Deliberate, by judgment. A snapshot_data tool lets the agent snapshot before anything else it judges risky — a bulk rewrite, a large seed, a data-munging step. Cheap enough that "when in doubt, snapshot" is the right habit.

If a later step breaks the data, the agent calls restore_data and rolls back — recovering from damage it can't fix by editing code. This is the capability the commit-time checkpoint can't provide: it catches damage done mid-run, before any commit. The agent can attempt the bold migration precisely because it can take it back.

1 — snapshot_data()

2 — the risky migration / bulk write

3 — verify: data broke?

4 — restore_data() if broken

fork@lsn + repoint, in place

Producer

neon-conn-proxy

Postgres / one timeline

A guardrail keeps this safe: snapshot and restore are not symmetric. A snapshot is cheap and non-destructive, so the agent takes it freely. A restore reverts data and restarts the compute, so the agent only uses it to undo its own in-run damage — reverting your historical data is a decision you make, not one the agent makes for you (more below).

Restore in place: the connection string never moves

For this to be usable mid-run — and mid-conversation — restore has to be stable. If it handed back a new database endpoint, every restore would mean reconfiguring the running app. So restore is in place: fork the timeline at the snapshot's LSN, then repoint the app's compute at the fork — same database name, same DATABASE_URL. The app doesn't reconnect to a new place; the data under it rolls back. It reuses the platform's existing fork-at-LSN + proxy-owned repoint path — the same primitive deploy and rollback ride on.

Undo you can ask for

The agent protecting itself ships first — that's live today. The same snapshots are meant to be yours, too, and that's the direction from here: because restore is stable and project-wide, it surfaces where you already are — the conversation. Say "that import wrecked everything, roll it back," and the chat agent proposes the restore ("I can roll your data back to the 2:14pm snapshot — want me to?") and you confirm; it never repoints your database on its own. A dashboard panel lists the restore points and is where that confirmation happens. The disruptive direction always stays behind a human yes.

And it sleeps for free

Why this belongs in every app, not just the busy ones: an Adorable app's database is scale-to-zero. Idle → the compute scales to zero replicas and costs nothing; the next request wakes it in seconds (see scale-to-zero Postgres). The snapshots are markers, so an app with 200 of them and no traffic is still ~free. A wedding RSVP tool, a seasonal storefront, an internal tool used twice a week — each can carry full snapshot/undo and contribute ~$0 while idle. It's the property that makes AI-app fleets viable at all (app.build, QwikBuild, Atoms run thousands of scale-to-zero databases for exactly this reason); here we spend that headroom on a safety net rather than just on idle savings.

One restore point for the whole application

Restore rolls back the whole project's database — and that's the right grain, not a limitation. On Adorable a project is one application, and its apps are microservices that deliberately share a database and reference each other's data. Rolling one service's data back without the others would break those cross-service references, so a consistent undo has to cover the whole application at once — the same reason you'd never point-in-time-restore half of a system. The finer grain — independent undo for each end user of a multi-tenant app you've built — is a separate shape (a branch per tenant) and is deferred.

Why it matters

Data-undo turns destructive automation from a liability into a capability. The agent can run the scary migration, because the floor is a one-tool-call restore to a state that actually existed; and you can undo a bad day's data by asking. Git gave code that property a long time ago. The database just needed the same primitive — a branch at an LSN — and an agent that knows to reach for it before it does something it might regret.

Build on the platform these posts describe.

Describe your app in plain English — Adorable writes the code, sets up the database, and ships it live.

Start building free