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

Written with an AI agent and checked against the guide above.