How to Add Backups to an AI-Built App
Platform snapshots cover the disaster you will probably never have and miss the ones you will. Storage bucket sync, row-level recovery with the partial index detail agents get wrong, and a twenty minute restore drill whose output is a written runbook.
Adding backups to an AI-built app takes about an hour and most of that hour is not the backup. Your hosting platform almost certainly snapshots the database already, and that covers the disaster nobody has: the whole server disappearing. It does not cover the disasters people actually have, which are one customer's records deleted by a bad migration on a Tuesday afternoon, an uploaded-files bucket that no snapshot ever touched, and a restore procedure nobody has run. This walks through the three layers you need, the restore drill that proves they work, and the specific gaps that agent-generated apps tend to ship with.
The question that sorts everything
Not "do you have backups". Ask: if a customer emails at 4pm saying their project list is empty and it was fine this morning, what do you do next?
If the honest answer involves restoring a whole-database snapshot over live production and losing every other customer's afternoon, you do not have a usable backup. You have a disaster recovery position, which is a different and much narrower thing. Almost every incident that will actually happen to a small app is partial: one table, one tenant, one bad bulk update.
Layer one: confirm what the platform already does
Before building anything, find out what you have. On a managed Postgres platform this is usually a daily snapshot with some retention window, and on paid tiers often point-in-time recovery.
Three things to check and write down:
The retention period. Seven days is common on lower tiers. A corruption discovered on day nine is not recoverable from a seven day window, and slow-burn data problems are routinely noticed late.
Whether point-in-time recovery is included on your plan or is an upgrade. The difference between "restore to last night" and "restore to 14:32" is the difference between losing a day and losing a minute.
What the restore actually produces. Usually a new database instance, not an in-place rollback. That is good news, because it means you can restore beside production and copy out just the rows you need.
Write the answers in your repository, not in a note to yourself. The person doing the restore may be you at 2am, and you will not remember which tier you are on.
Layer two: the thing platform backups miss
Your object storage bucket. Uploaded images, generated PDFs, user avatars, exports. Database snapshots back up the rows that reference those files, not the files themselves. Restore the database and you get a table full of URLs pointing at objects that may or may not still exist.
Agent-built apps have this gap more often than hand-built ones, because the storage integration typically arrives as a working upload flow with no lifecycle policy attached. It works, so nobody looks at it again.
The cheap fix is a scheduled sync to a second bucket in a different account or region:
# nightly, from a small scheduled job or CI runner
aws s3 sync s3://myapp-uploads s3://myapp-uploads-backup \
--delete-excluded \
--only-show-errors
# for a Supabase or S3-compatible bucket, same tool, different endpoint
aws s3 sync s3://myapp-uploads s3://myapp-uploads-backup \
--endpoint-url https://<project>.storage.example.comDeliberately do not pass `--delete`. A sync that mirrors deletions faithfully will faithfully mirror the accidental one. Let the backup bucket grow and prune it on a schedule that is longer than your detection window.
If your files are user-generated and regulated, check that the backup region is one you are allowed to store them in before you enable this. A backup in the wrong jurisdiction is a compliance problem you created while solving a reliability one.
Layer three: row-level recovery you control
This is the layer that handles the 4pm email, and it is the one no platform gives you.
The pattern is soft deletion plus an audit trail. Instead of removing rows, mark them, and record who changed what:
alter table projects add column deleted_at timestamptz;
create index projects_live_idx on projects (owner_id)
where deleted_at is null;
-- application queries filter on it
select * from projects
where owner_id = $1 and deleted_at is null;Now an accidental deletion is a one-line reversal rather than a restore operation. The index with the `where` clause matters: without it, every read pays for the soft-deleted rows forever.
Two cautions that agents get wrong when they generate this. First, unique constraints. If `slug` is unique and a soft-deleted row still holds it, the user cannot recreate a project with the same name. Use a partial unique index so only live rows compete:
create unique index projects_slug_live_idx on projects (owner_id, slug)
where deleted_at is null;Second, cascades. A soft delete on a parent does not hide its children, so a deleted project's tasks stay visible unless you handle it explicitly, either by filtering through a join or by marking children in the same transaction.
We covered the user-facing half of this in adding soft delete and undo to an AI-built app, and the change history half in adding an audit log. Together they turn most incidents into a support task rather than an outage.
The restore drill
A backup that has never been restored is a hypothesis. Test it once, properly, and put twenty minutes in the calendar quarterly.
Restore last night's snapshot to a new instance. Note how long it took, in real minutes, not the marketing number.
Connect to it. Confirm the schema matches production, including recent migrations. A snapshot from before a migration restores an older schema, and your current application code will not run against it.
Pull one specific customer's rows out and write them into a scratch table. This is the skill you actually need, and the first time you attempt it should not be during an incident.
Verify a file. Take a storage URL from the restored rows and fetch it from the backup bucket. This is where people discover the storage gap.
Write down the sequence you just performed as a runbook, with the exact commands. Delete the restored instance.
The output of the drill is not confidence, it is a document. Add it to whatever you keep as an incident response plan, because a restore procedure discovered live is a restore procedure performed badly.
What to tell your coding agent
If you are having an agent implement this, be specific, because the defaults it will choose are the ones above that break.
Ask for soft deletion with partial indexes on every unique constraint, explicit handling of child records, and application queries updated everywhere rather than only in the obvious place. Ask it to list the files it changed so you can check nothing outside the data layer moved. And have it write the migration as reversible, since a backup change that cannot itself be rolled back is a poor start.
One thing to keep off the agent's plate entirely: credentials for the backup destination. A second bucket exists so that a compromise or mistake in the primary environment does not reach it, and that property is lost the moment the same key can write to both.
When this is not enough
Everything above suits a small to mid-size app with one database and one bucket. Three situations need more.
If you hold data under a retention obligation, retention is a legal requirement with a defined period, and a rolling seven day window does not satisfy it regardless of how good your restore drill is.
If your database is large enough that a full restore takes hours, per-tenant logical exports on a schedule will serve you better than snapshots alone. Our guide to adding data export is most of that machinery already.
And if you are still choosing your stack, backup and restore behaviour is a legitimate selection criterion that rarely appears on comparison pages, which is one of the points in choosing a database for an AI-built app.
FAQ
Does my hosting platform's automatic backup cover everything?
No. It covers the database, usually not object storage, and it restores whole instances rather than individual rows. Both gaps are the ones that come up in real incidents.
How often should backups run?
Match the interval to how much work you can afford to lose. Daily snapshots plus point-in-time recovery is the common answer for small apps. The storage bucket sync can be nightly, since files change less than rows.
Is soft delete a backup?
Not on its own, but it handles the most frequent recovery case: something deleted that should not have been. Pair it with real snapshots, which cover the cases soft delete cannot, such as a corrupted table or a bad migration.
How do I restore one customer's data without touching everyone else's?
Restore the snapshot to a separate instance, extract that customer's rows there, then insert them back into production. Never restore a snapshot over a live database to fix a single-tenant problem.
How often should I test a restore?
Once when you set it up and quarterly after that. Twenty minutes, and the deliverable is a written runbook with the real commands and the real timings.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


