How to back up your Supabase database AND Storage (the right way).
Supabase's built-in backups won't save you from the thing that actually kills no-code founders — an accidental delete or an AI edit dropping a table — and they don't touch your Storage files at all. Here's a calm, ordered way to get a real, offsite backup of both.
Take your own logical dump of the database with supabase db dump or pg_dump, export your Storage buckets separately (they're not in a database dump) with rclone against the S3 endpoint or a download script, push both offsite to a different cloud, and — the part everyone skips — test the restore on a scratch project before you need it.
Why you can't rely on Supabase's built-in backups alone
Supabase does take platform backups, and on paid plans you can get point-in-time recovery. That's genuinely useful — but it solves a different problem than the one most founders actually hit. Three gaps matter:
- Lower tiers have limited or no point-in-time recovery. Free and the cheaper paid plans get, at best, a daily backup with a short retention window. If something went wrong three days ago and you only noticed today, that backup may already be gone.
- Daily backups don't protect against you. A platform backup protects against Supabase losing your data. It does nothing about an accidental
delete from orderswith nowhereclause, or an AI assistant "tidying up" your schema and dropping a table. The mistake gets faithfully copied into the next backup, and the good data ages out. - Storage is not in the database backup at all. Your files — user uploads, images, PDFs, anything in a bucket — live in object storage, not in Postgres. A database backup never contains them. Lose the project and the database backup brings back your rows but none of your files.
The point of a real backup is that it's offsite (somewhere other than the Supabase project it protects) and independent (something you can restore yourself, without needing the original account to still exist). That's what the rest of this guide sets up.
Part 1 — Back up the Postgres database
A database backup is a logical dump: a file (usually SQL) that can recreate your schema and data anywhere. There are two easy ways to make one.
Option A — the Supabase CLI
If you have the Supabase CLI installed and linked to your project, the one-off dump is a single command:
# schema + data in one file
supabase db dump --linked -f backup.sql
# or split them — schema first, then just the data
supabase db dump --linked -f schema.sql
supabase db dump --linked --data-only -f data.sql
# include roles (useful for a full project restore)
supabase db dump --linked --role-only -f roles.sql
Splitting schema from data is worth doing: the schema file lets you rebuild empty tables fast, and the data file restores the rows on top. The --role-only dump captures database roles, which a clean restore into a new project will want.
Option B — pg_dump with the connection string
If you'd rather use Postgres' own tooling, grab the connection string from Project Settings → Database. Supabase gives you a pooled connection string (via the connection pooler) and a direct one — for a dump, use the connection string Supabase labels for this purpose and run:
pg_dump \
"postgresql://postgres.<ref>:[YOUR-PASSWORD]@aws-0-<region>.pooler.supabase.com:5432/postgres" \
--no-owner \
--no-privileges \
--schema=public \
-f backup.sql
--no-owner and --no-privileges strip ownership/grant statements that reference roles which may not exist in the target — that makes the dump far easier to restore into a fresh project. Limit to --schema=public if you only want your own tables; drop that flag to capture more. For a compressed, faster-to-restore archive, use the custom format instead:
pg_dump "postgresql://...pooler.supabase.com:5432/postgres" \
--no-owner --no-privileges -Fc -f backup.dump
Restoring the dump
A plain SQL dump restores with psql; a custom-format (-Fc) archive restores with pg_restore:
# plain .sql
psql "postgresql://...new-project.../postgres" -f backup.sql
# custom-format .dump
pg_restore --no-owner --no-privileges \
-d "postgresql://...new-project.../postgres" backup.dump
Automating it on a schedule
A backup you take by hand once is a backup you'll forget to take next month. The idea is to run the dump on a schedule and push the file somewhere offsite automatically — a nightly cron job on a small server, or a free GitHub Action on a schedule. The shape of it:
# .github/workflows/backup.yml
name: nightly-db-backup
on:
schedule: [{ cron: "17 3 * * *" }] # 03:17 UTC daily
jobs:
dump:
runs-on: ubuntu-latest
steps:
- run: |
pg_dump "${{ secrets.SUPABASE_DB_URL }}" \
--no-owner --no-privileges -Fc \
-f "db-$(date +%F).dump"
# then upload the file offsite — e.g. to S3/B2/GCS:
- run: aws s3 cp db-*.dump s3://my-backups/supabase/
Keep the connection string in the runner's encrypted secrets (here SUPABASE_DB_URL), never in the workflow file itself.
service_role key in client-side code, a public repo, or a chat prompt. The connection string is full database access; the service_role key bypasses Row Level Security entirely. They belong only in server-side secrets or an encrypted CI store. If one leaks, rotate it in the Supabase dashboard immediately.Don't want to babysit cron jobs and restore tests?
If your database or your files just disappeared, send me what happened and I'll help recover it — flat fee, quoted up front, reply the same day. Most data rescues are sorted within a day or two.
Get my data recovered → Flat fee · reply today · no retainer requiredPart 2 — Back up Storage (the part people miss)
This is the gap that catches people out. Supabase Storage is object storage — your buckets and files sit alongside Postgres, not inside it. So none of the database dumps above contain a single byte of your actual files. You have to export Storage separately. There are two solid approaches.
Approach A — rclone against the S3-compatible endpoint
Supabase Storage exposes an S3-compatible endpoint, which means battle-tested sync tools work against it. rclone can mirror every bucket to another cloud in one command. Configure a remote with your project's S3 endpoint and S3 access keys (Project Settings → Storage), then:
# one-time: rclone config — add an "s3" remote pointing at
# https://<ref>.supabase.co/storage/v1/s3 (region from your dashboard)
# then mirror all buckets to another cloud you control
rclone sync supabase-s3: backup-remote:supabase-storage \
--progress --transfers 8
Run that on the same schedule as your database dump and your files land offsite alongside your data.
Approach B — list and download with supabase-js
If you'd rather script it, list the objects in each bucket and download them one by one. Using supabase-js with the service_role key (server-side only):
import { createClient } from "@supabase/supabase-js";
import { writeFile, mkdir } from "node:fs/promises";
const sb = createClient(process.env.SUPABASE_URL, process.env.SERVICE_ROLE_KEY);
const { data: buckets } = await sb.storage.listBuckets();
for (const b of buckets) {
const { data: files } = await sb.storage.from(b.name).list("", { limit: 10000 });
await mkdir(`backup/${b.name}`, { recursive: true });
for (const f of files) {
const { data } = await sb.storage.from(b.name).download(f.name);
await writeFile(`backup/${b.name}/${f.name}`, Buffer.from(await data.arrayBuffer()));
}
}
(For nested folders you'd recurse into each prefix — but for most apps a flat list per bucket is the bulk of it.)
storage schema), so capture them in your database dump or write them down. Restoring files into a bucket with the wrong policy can quietly expose private uploads.Part 3 — Where to store the backups
A backup sitting in the same account as the thing it protects isn't really a backup. The plain-English version of the classic 3-2-1 rule:
- 3 copies of anything you can't afford to lose (the live data counts as one).
- 2 different places — e.g. Supabase plus one other cloud or your own machine.
- 1 of them offsite, in a different cloud or account than Supabase — so a billing lockout, a deleted project, or a compromised account can't take your backup with it.
Then two more habits: encrypt the backups at rest (they contain real customer data), and set a retention policy — keep, say, the last 14 daily and a few monthly copies — so a mistake you notice late is still recoverable, without your storage bill growing forever.
Part 4 — Test your restore (this is the whole point)
A backup you've never restored is a guess, not a backup. Plenty of people discover their dump was empty, truncated, or missing a table only on the day they desperately need it. Test it while everything is calm:
- Spin up a scratch Supabase project (or a local Postgres).
- Restore the latest dump into it with
psql/pg_restore. - Verify row counts against production for your key tables (
select count(*) from orders;), and open a few files from your Storage export to confirm they're intact and not zero-byte.
Do this once after you set up the backup, and again whenever your schema changes meaningfully. A ten-minute test now is the difference between a bad afternoon and a closed business.
Part 5 — The RLS, policies and secrets caveat
A logical dump captures your tables and data, but not necessarily everything that makes your app work. Watch for:
- RLS policies.
pg_dumpgenerally includes the policies on tables it dumps, but if you scope the dump narrowly (or skip the schema), you can lose them. Confirm your policies are present in the restored project — an unprotected table is a data leak. - The
authschema and other system schemas. A default--schema=publicdump won't include Supabase'sauthusers or thestoragemetadata. For a true full-project backup, dump those schemas too (or accept that you'll re-provision auth) — and know that some managed internals can't be moved verbatim. - Secrets and env vars. Your Edge Function secrets, API keys, and project settings are not in a database dump. Keep a separate, secure record of them so a rebuilt project isn't missing its keys.
The honest version
Everything above works. It's also a standing chore: scheduled database dumps, a separate Storage export, offsite uploads with encryption and retention, plus periodic restore tests so you actually know it works. Most non-technical founders set it up once, then quietly stop babysitting it — and find out it broke at the worst possible moment.
That's exactly the job Backstop's offsite backup tier does for you: it backs up your Supabase database and your Storage files, keeps them offsite in a separate account, and gives you a tested, one-click restore — so recovering from a bad delete or a dropped table is a button, not a panicked weekend.
Get offsite backups that include your files — and a tested restore.
Join the waitlist for Backstop — monitoring + offsite backup built for Supabase and no-code apps. We'll email you at launch, nothing else.
Frequently asked
Doesn't Supabase already back up my database for me?
It takes platform backups, but the coverage depends on your plan — free and lower tiers get limited daily backups with little or no point-in-time recovery. And even where backups exist, they protect against Supabase losing your data, not against you (or an AI assistant) deleting a table or overwriting rows. Those mistakes get copied straight into the backup. Keep at least one backup you control, offsite.
Does a database dump include my Storage files?
No. A pg_dump or supabase db dump captures the Postgres database only — tables, rows, schema. Storage buckets are object storage that lives outside Postgres, so the files are never in a database dump. Export Storage separately with rclone against the S3 endpoint or a script that lists and downloads every object.
How do I back up my Storage buckets?
Treat Storage as object storage and copy the files out. The simplest robust route is rclone pointed at Supabase's S3-compatible endpoint, which syncs every bucket to another cloud. Or list objects with the Storage API / supabase-js and download each one. Capture the bucket settings and access policies too — they aren't part of the file data.