Backing up Supabase to a NAS — sharing how I set it up
I've seen a few people ask how to keep a copy of a Supabase project somewhere that isn't Supabase, and never found a straight answer, so I (and Claude) built it for my own project and figured I'd write it up.
Disclaimer: This post is written by Claude with me behind the wheels, otherwise this would read as an unorganized mess 🤭
The gap I cared about: Supabase's own backups live inside the same Supabase project. That's fine for a bad migration, but not for the project being deleted, an account suspended, or a billing problem. Worth knowing too — point-in-time recovery doesn't cover Storage objects at all. It restores the rows pointing at your files, not the files.
The setup
pg_dump for the database, rclone for the storage bucket, both encrypted with age before they leave the machine, written to a Synology NAS I already owned. Four runs a day, 7-day retention. A plain shell script driven by Synology's Task Scheduler — no agent, no cloud service, nothing to pay for.
How it actually works
Database, three dumps via the Supabase CLI:
supabase db dump --db-url "$URL" -f roles.sql --role-only
supabase db dump --db-url "$URL" -f schema.sql
supabase db dump --db-url "$URL" -f data.sql --use-copy --data-only \
-x auth.sessions -x auth.refresh_tokens -x auth.flow_state \
-x auth.one_time_tokens -x auth.schema_migrations -x auth.audit_log_entries
Two things there took me a while. Connect through the session pooler on port 5432 — the direct db.<ref>.supabase.co host is IPv6-only, and port 6543 (transaction mode) can't run pg_dump. And exclude the auth session churn but keep auth.users, auth.identities and auth.mfa_factors — the schema dump skips the auth schema, but a --data-only dump includes its rows, and that's what makes the backup restorable at all. Without them you restore a database nobody can log into.
Then tar the three files and encrypt:
tar -czf - roles.sql schema.sql data.sql | age -r age1... -o backup.tar.gz.age
Storage, via Supabase's S3-compatible endpoint (force_path_style = true, list_version = 2):
rclone sync supa:report-images /volume1/backup/storage/report-images \
--backup-dir /volume1/backup/storage-deleted/$(date +%F)/report-images
--backup-dir is the bit that turns a mirror into a backup — deleted or overwritten files move aside instead of vanishing. Retention is just rclone delete --min-age 7d on both directories.
On the Synology: Container Manager is required, because the Supabase CLI runs pg_dump inside a container matching your Postgres version. rclone, age and supabase are single static binaries that run natively on DSM. Task Scheduler runs the whole thing as root, and that's also your shell if you'd rather not enable SSH.
A few choices that matter more than the tooling
Encrypt to a public key. The NAS holds only the public half, so the backup machine can create archives but can never read one.
Keep the encryption keys out of the backup. My app encrypts personal data with keys held in Supabase Vault, and those dump as ciphertext wrapped by a key that lives elsewhere — so a restore into a fresh project can't read a single encrypted column. They're escrowed separately, offline, under a different key. Easy to get wrong, and you'd only find out during a restore.
Actually do a restore. I rebuilt the whole thing into a throwaway project twice — database, keys, images, logins. Two things I'd have got wrong otherwise:
SET session_replication_role = replica before loading the data. My organizations and users tables reference each other, so with foreign keys enforced there's no row order that works and the restore fails on the first row.
And the storage step was wrong in a way that fails silently: without rclone --ignore-times it restores no images at all, because the database dump already recreated the storage metadata, so rclone sees matching names and sizes and skips everything. Object count and byte total both report success. You find out when someone opens a page and the image 404s.
Last piece: a heartbeat. Notifications alert only on abnormal termination, so silence means success — which means a powered-off NAS and four healthy backups look identical. A dead-man's switch pinged on success is the only alarm that fires on absence. Set the expected interval from the longest gap in your schedule, not the average, or it cries wolf nightly and you'll mute it. And make it an interval, not a daily quota — a quota is satisfied by a burst at 3am. I already use betterstack for uptime monitoring so adding a heartbeat was easy
IMPORTANT!
Whatever you build, restore it once before you trust it. The backup half is easy. The restore is where the surprises live.