The Supabase Free plan doesn't give you access to daily backups. Access starts on Pro. The official advice for Free projects is to export your data regularly with the CLI and keep a copy somewhere else. Manual exports are easy to forget.
A prototype or side project can become a tool a client relies on. Free projects can still be paused after a week of inactivity.
Here's the setup I'd use: a GitHub Actions workflow that runs once a week and writes the dump to a bucket you own.
The workflow
You need a private GitHub repo, a bucket on Cloudflare R2, AWS S3 or Backblaze B2, and your project's Session pooler connection string (dashboard, Connect button, Session pooler).
Save this as .github/workflows/scheduled-backup.yml:
name: scheduled-backup
on:
schedule:
- cron: "0 3 * * 0" # Sundays at 03:00 UTC
workflow_dispatch: {} # run it by hand from the Actions tab
permissions:
contents: read
concurrency:
group: backupdrill-scheduled
cancel-in-progress: false
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Install postgresql-client-17 (PGDG)
run: |
sudo install -d /usr/share/postgresql-common/pgdg
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| sudo gpg --dearmor -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg] \
https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update
sudo apt-get install -y postgresql-client-17
- name: Tell backupdrill where pg_dump 17 lives
run: echo "BACKUPDRILL_PG_DUMP=/usr/lib/postgresql/17/bin/pg_dump" >> "$GITHUB_ENV"
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run backupdrill
run: npx -y backupdrill@2.10.1 backup
env:
BACKUPDRILL_DATABASE_URL: ${{ secrets.BACKUPDRILL_DATABASE_URL }}
BACKUPDRILL_S3_BUCKET: ${{ secrets.BACKUPDRILL_S3_BUCKET }}
BACKUPDRILL_S3_ACCESS_KEY_ID: ${{ secrets.BACKUPDRILL_S3_ACCESS_KEY_ID }}
BACKUPDRILL_S3_SECRET_ACCESS_KEY: ${{ secrets.BACKUPDRILL_S3_SECRET_ACCESS_KEY }}
BACKUPDRILL_S3_ENDPOINT: ${{ secrets.BACKUPDRILL_S3_ENDPOINT }}
BACKUPDRILL_S3_REGION: ${{ secrets.BACKUPDRILL_S3_REGION }}
BACKUPDRILL_PING_URL: ${{ secrets.BACKUPDRILL_PING_URL }}
The PGDG step is there because pg_dump's major version has to be at least your Postgres version, and the client on the runner can be older than your database.
Add the four required values as repository secrets: the pooler string with your password in it, the bucket name, and an access key pair scoped to that one bucket. R2 and B2 also need the endpoint and region. Then open the Actions tab and run it once by hand.
backupdrill is an open-source CLI that wraps pg_dump and writes a checksummed manifest next to the dump. The dump itself is plain custom format, so stock pg_restore can read it. If you'd rather call pg_dump yourself, the rest of this post still applies.
Why weekly and not daily
Every backup streams your data out of Supabase and counts against the Free plan's 5 GB of monthly egress, which your app shares. Free databases are capped at 500 MB. If each backup moved the full 500 MB, four weekly runs would use about 2 GB and 30 daily runs about 15 GB, three times the allowance. Your real number depends on your database.
Keep workflow_dispatch so you can take an extra backup before a risky migration.
Check that the backups keep running
A public repo. GitHub turns off scheduled workflows in public repos after 60 days without activity, and a backup repo is exactly the kind of repo that goes quiet. Private repos don't have that rule.
An unchecked Actions tab. A failed scheduled run is easy to miss. Create a weekly Healthchecks.io check, choose who gets the alert, and store its ping URL as the BACKUPDRILL_PING_URL secret. The workflow above already passes it to the backup step.
A green run isn't a restore
Green means the backup command finished successfully. It hasn't tested whether the dump restores. Before you trust this, restore a snapshot somewhere disposable, either with pg_restore into a scratch Postgres or with backupdrill drill, which pulls the latest snapshot into a throwaway Docker Postgres and checks it against the manifest.
Also check what's in scope. The dump covers the public schema by default, and it doesn't bring back Storage files. Those need their own S3 keys in the workflow.
The full guide to Supabase backups with GitHub Actions has the Storage setup, the bucket click-paths and what happens if you go over the egress allowance.
I work on BackupDrill, so I'm biased toward testing restores. The CLI is MIT licensed: https://github.com/backupdrill/cli
I used AI to help draft this post.
Top comments (1)
Great call making a restore drill part of the backup definition rather than treating a green workflow as success. I'd also stamp each manifest with the app's migration revision and validate roles/extensions on restore; a dump can load cleanly yet still leave the deployed app unable to use the restored schema. Are you recording a schema revision per snapshot today?