How to restore a Supabase backup — and roll back bad changes
Restores happen on bad days, so this page is organized by bad day. A migration just corrupted data and the project has PITR: use PITR — that is what it is for. The whole database needs to go back to last night: dashboard restore on Pro, or your own dump into a fresh project. One table needs yesterday’s rows and everything else is fine: selective restore — most people don’t know it exists, and it saves the rest of today’s data. Two facts before any button gets pressed: an in-place restore erases everything written after the restore point, and no database restore — Supabase’s or yours — brings back Storage files.
First, always: dump what you have now
Before any restore, take a fresh dump of the current state — a damaged database can usually still be dumped, and it is the only copy of the application data written since your backup. (A --schema=public dump: your tables are in it; Auth users and Storage metadata added since are not.) The restore you are about to run overwrites all of it. Thirty seconds of pg_dump (working command here) buys you the option to recover individual rows from the “bad” state later — and after incidents, someone almost always asks for exactly that.
Path 1 — dashboard restore (Pro and above)
Dashboard → Database → Backups, pick a day, restore. It runs in place: the project is inaccessible while the restore runs, downtime scales with database size, and you should drop active subscriptions and replication slots first (Realtime’s own slot is handled automatically). Granularity is daily — a bad deploy at 5 pm restored from last night costs the day’s writes across every table, which is why the single-table path below exists.
One official option most people miss: on paid plans with physical backups, Supabase can restore a backup — or a PITR point — into a new project instead of in place. Schema, data, roles, and Auth users come across; Storage objects, Edge Functions, and auth configuration do not — those move manually. It is the safer shape when the damaged project needs to stay up as evidence, at the cost of re-pointing your application. On the Free plan this page of the dashboard has nothing to offer either way: there are no backups to restore.
Path 2 — PITR, if you bought it
The $100/month-per-project add-on recovers to any second in its retention window: roll back to 14:03:27, right before the migration ran. For “we broke it minutes ago and every write since matters”, nothing else on this page competes. It is database only, it replaces the daily backups rather than adding to them, and it recovers in place or — like the daily backups — into a new project on physical-backup projects. Where it wins and where it doesn’t: the honest comparison.
Path 3 — your own dump, into a fresh project
If you have your own backups — from pg_dump, a scheduled workflow, or a backup service — the recovery target is a fresh, empty Supabase project, never the damaged one. Restoring over a half-broken database leaves you with objects from both eras and no way to tell which is which; with a fresh project, the damaged one stays untouched as evidence and fallback. If your schema uses extensions beyond Supabase’s defaults — pgvector, PostGIS — enable them on the target first (Database → Extensions): tables built on those types cannot restore without them. Then:
read -rs PGPASSWORD && export PGPASSWORD # hidden prompt — nothing in shell history
pg_restore --no-owner --no-privileges \
--dbname "postgresql://postgres.<target-ref>@<pooler-host>:5432/postgres" \
backup-2026-08-08.dump
unset PGPASSWORDThe flags are not decoration:
--no-owner --no-privileges— the dump records owners and grants for roles that differ between projects; without the flags the restore drowns in ownership errors.- Never
--cleanagainst Supabase. Dropping and recreatingpublicsilently wipes the default grants foranonandauthenticated— the restore looks successful and the API answers 401 on everything. - One error is expected and harmless:
schema "public" already exists. Errors naming your own tables, indexes, or constraints are real findings.
Honest scope note: a --schema=public dump restores your application schema and data. Auth users, secret values, and Edge Functions are not in it — users get re-invited or migrated separately, secrets get re-entered, functions get redeployed. Budget for that in your recovery time. If your backups are BackupDrill snapshots, the restore & recovery docs cover the console wizard (a read-only preflight before anything executes) and the CLI path, which also puts Storage files back into the target project and reconciles them key by key.
Rolling back one table, not the whole database
Most “I need to restore” moments are really “one table needs to go back”. Restoring the entire database to yesterday to fix orderscosts you today’s data in every othertable — a trade nobody actually wants. Custom-format dumps can restore selectively. Bring yesterday’s table up in a scratch Postgres next to production:
docker run -d --rm --name rollback-scratch \
-e POSTGRES_PASSWORD=scratch \
-p 127.0.0.1:55433:5432 \
postgres:17-alpine
# Wait until it accepts TCP connections
until docker exec rollback-scratch pg_isready -h 127.0.0.1 -U postgres; do sleep 1; done
pg_restore --no-owner --no-privileges \
--table=orders \
--dbname "postgresql://postgres:scratch@127.0.0.1:55433/postgres" \
backup-2026-08-08.dumpNow you have yesterday’s ordersand production’s, side by side, and the fix becomes a data question instead of a restore question: diff them, decide exactly which rows move, and apply that change to production in a transaction you can inspect first. For a “these rows were deleted, put them back” case that can be as small as a \copy out of the scratch table and a \copy into production. Slower than a big red restore button, and it only endangers the rows you chose. One caveat: --table restores the table alone, not the types it depends on — if the restore errors on a missing enum or extension type, restore the whole archive into the scratch container instead. It is throwaway; a full restore there is never wrong, just slower. When you are done: docker rm -f rollback-scratch.
The restore that works is the one you rehearsed
Everything above assumes the backup restores. That assumption fails quietly: a dump taken with the wrong role, a version-skewed pg_dump, a job that half-failed months ago — all look like good backups in a bucket until the day they matter. Rehearsing the restore is cheap and mechanical: fifteen minutes by hand in Docker, or automated on a schedule. That is the product we run — here is a real drill report showing exactly what gets verified — but the manual drill needs nothing from us, and doing it once before an incident beats reading this page during one.
FAQ
How do I roll back my Supabase database?
Depends how precisely you need to land. PITR (the $100/month add-on) recovers to any second in its window — that is what it exists for. The Pro plan's daily backups restore in place from the dashboard, at last-night granularity, erasing everything written since. Your own dumps restore into a fresh project with pg_restore, and you switch the app over. Whichever path: take a fresh dump of the current state first — even a damaged database can usually still be dumped, and it is the state you are about to overwrite.
Can I restore a Supabase backup to a different project?
Two ways. On paid plans with physical backups, Supabase's restore-to-a-new-project feature clones a backup or PITR point into a fresh project — schema, data, roles, and Auth users included; Storage objects, Edge Functions, and auth configuration still move manually. And your own pg_dump archives restore into any fresh, empty project with pg_restore, on every plan. What no path offers is restoring into an arbitrary existing project with data already in it.
Why does my restored project answer 401 on every API request?
Almost always: the restore ran with --clean. Dropping and recreating the public schema silently wipes the default grants Supabase gives anon and authenticated, so PostgREST can no longer see your tables and every API call returns 401. Restore into a fresh project without --clean instead. The expected 'schema "public" already exists' error is harmless; missing grants are not.
Does restoring a backup bring back deleted Storage files?
No. A database restore — dashboard, PITR, or pg_restore — only recovers storage.objects, which is metadata. The actual files live in a separate object store. If they were not backed up separately, the restored database will point at files that no longer exist: every SQL query looks healthy and every download URL 404s.
Sources
- Supabase docs — Database Backups
- Supabase docs — Manage PITR usage
- Supabase — Pricing
- Supabase docs — Connecting to your database
- PostgreSQL docs — pg_restore
Facts and prices last verified August 9, 2026 against the sources above. Written by the team behind BackupDrill.
Want every backup restore-tested on a schedule, so the bad day holds no surprises? Open the console — the free plan covers backups for one project; weekly restore drills start on Solo — or run the open-source CLI yourself.