VaultKeep blog

Supabase Free tier has no backups. Here's a free fix in ten minutes

September 17, 2026 · 8 min read · by VaultKeep

A GitHub Actions workflow that pg_dumps your Supabase database every night, encrypts it, uploads it to any S3-compatible bucket, and test-restores it — with no servers and no bill.

Free Supabase projects have no automated backups. Pro projects get seven days of daily snapshots inside Supabase. Either way, if you want a copy that lives somewhere else and that you know restores, you have to make it yourself.

This is the smallest setup we'd trust: a GitHub Actions workflow on a schedule. It runs pg_dump, encrypts the archive, uploads it to an S3-compatible bucket (Cloudflare R2's free tier is 10 GB and has no egress fees; AWS S3, Backblaze B2 or MinIO work the same), and then actually restores the dump into a throwaway Postgres to prove it's usable. Everything runs on GitHub's free minutes.

What you need

Store these as repository secrets: SUPABASE_DB_URL, S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, BACKUP_PASSPHRASE.

The workflow

Save this as .github/workflows/backup.yml:

name: supabase-backup
on:
  schedule:
    - cron: "17 3 * * *"   # 03:17 UTC nightly; avoid :00 to dodge the rush
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    services:
      # a throwaway Postgres for the restore test
      pg:
        image: postgres:17
        env: { POSTGRES_PASSWORD: test }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s --health-timeout 5s --health-retries 20
    steps:
      - name: Install Postgres 17 client
        run: |
          sudo apt-get install -y postgresql-common
          sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
          sudo apt-get install -y postgresql-client-17

      - name: Dump
        env: { DB: ${{ secrets.SUPABASE_DB_URL }} }
        run: |
          STAMP=$(date -u +%Y%m%dT%H%M%SZ)
          echo "STAMP=$STAMP" >> "$GITHUB_ENV"
          pg_dump "$DB" --format=custom --compress=6 --no-owner --no-privileges \
            --file "backup-$STAMP.dump"
          ls -l backup-$STAMP.dump

      - name: Restore test into throwaway Postgres
        run: |
          # Supabase-managed schemas exist only on Supabase; restore the rest.
          pg_restore --no-owner --no-privileges --jobs 2 \
            --exclude-schema=auth --exclude-schema=storage --exclude-schema=extensions \
            --exclude-schema=realtime --exclude-schema=supabase_functions --exclude-schema=vault \
            --exclude-schema=graphql --exclude-schema=graphql_public --exclude-schema=pgsodium \
            --exclude-schema=net --exclude-schema=cron --exclude-schema=supabase_migrations \
            --dbname "postgresql://postgres:test@localhost:5432/postgres" "backup-$STAMP.dump" || true
          TABLES=$(psql "postgresql://postgres:test@localhost:5432/postgres" -tAc \
            "select count(*) from information_schema.tables where table_schema='public'")
          echo "restored public tables: $TABLES"
          test "$TABLES" -gt 0   # fail the run if nothing came back

      - name: Encrypt
        env: { PASS: ${{ secrets.BACKUP_PASSPHRASE }} }
        run: |
          openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt \
            -in "backup-$STAMP.dump" -out "backup-$STAMP.dump.enc" -pass env:PASS
          rm "backup-$STAMP.dump"

      - name: Upload
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: auto
        run: |
          aws s3 cp "backup-$STAMP.dump.enc" \
            "s3://${{ secrets.S3_BUCKET }}/supabase/backup-$STAMP.dump.enc" \
            --endpoint-url "${{ secrets.S3_ENDPOINT }}"

Run it once by hand from the Actions tab. If the restore step reports zero tables, fix that before trusting the schedule.

What this gives you

What it doesn't give you

Be clear-eyed about the gaps, because they're the reason backup services exist:

If that list is fine for your project, use the workflow. Seriously, use it; it beats nothing by a mile.

If it isn't, that list is roughly what VaultKeep adds: Storage sync, retention with crypto-shredding, missed-window alerts, weekly verification that compares row counts against a manifest, one-click restore into a fresh Supabase project, and a per-backup key wrapped by a master key the runner never stores. It's free for one project, and you can keep the GitHub workflow running alongside it. Two independent copies are better than one.

Want this without maintaining it?

VaultKeep does what this workflow does — encrypted, off-site, restore-tested every week — plus Storage buckets, retention, alerts and one-click restore. Free for one project, and you never touch YAML.

Start backing up free →