Backstop App broke? Get it fixed →
Supabase · Backups · Storage

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.

Updated June 2026 · ~9 min read

TL;DR

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:

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.

Never put the database connection string or your 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 required

Part 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.)

Don't forget the bucket metadata. The files are only half of Storage. You also want a record of each bucket's settings — public vs private, file-size limits, allowed MIME types — and the access policies that govern who can read and write them. Those live in the database (the 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:

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:

  1. Spin up a scratch Supabase project (or a local Postgres).
  2. Restore the latest dump into it with psql / pg_restore.
  3. 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:

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.