Back up a Supabase free-tier project with GitHub Actions

The Supabase Free plan does not include automated backups — those start on Pro. The official backup docs’ advice for free-tier projects fits in one sentence: export your data regularly with the CLI and keep off-site copies. This guide is that sentence turned into something that runs without you: a scheduled GitHub Actions workflow that dumps your database — and optionally your Storage files — into a bucket you own, every week, for free. Total setup time is about fifteen minutes, most of it copying secrets.

Why free-tier projects need their own backups more, not less

It is tempting to reason “it’s a free project, the stakes are low”. The mechanics point the other way:

  • Nobody else is backing it up. On Pro and above, Supabase takes daily backups of the database. On Free, if you do not export your data, no copy of it exists anywhere.
  • Free projects get paused after 1 week of inactivity. A paused project can be restored from the dashboard for 90 days; after that it can no longer be restored in place — you can still download the backup and Storage files from the dashboard and migrate them to a new project, but now you are doing recovery work on a deadline you did not choose. With your own snapshots in your own bucket, a pause is a non-event.
  • Free projects are exactly the unattended ones. Side projects, prototypes that quietly became real, the weekend tool a client started depending on — no one is on call for these, which is precisely why the backup has to run on a schedule instead of on memory.

Set it up, step by step

You need a GitHub repository (private is the right choice here — more on why below), a bucket you own on Cloudflare R2, AWS S3, or Backblaze B2, and your project’s Session Pooler connection string. The workflow uses the open-source backupdrill CLI (MIT, github.com/backupdrill/cli), which wraps pg_dump and writes a checksummed manifest next to the dump.

Step 1 — copy the workflow into your repo

Create .github/workflows/scheduled-backup.yml with the following content (a daily variant ships in the CLI repo as examples/scheduled-backup.yml — on the Free plan, keep the weekly cron below):

# .github/workflows/scheduled-backup.yml
name: scheduled-backup

on:
  schedule:
    # Weekly, Sunday 03:00 UTC — the sane default on the Free plan (see the
    # egress section). Runs can start late when GitHub Actions is under load.
    - cron: "0 3 * * 0"
  workflow_dispatch: {} # lets you trigger a backup by hand from the Actions tab

# Read-only checkout is all this job needs.
permissions:
  contents: read

# Never let two backups overlap.
concurrency:
  group: backupdrill-scheduled
  cancel-in-progress: false

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      # pg_dump's major version must be >= your Supabase Postgres version.
      # Supabase projects run PG 15 or 17 — postgresql-client-17 covers both.
      # Install it from the official PostgreSQL apt repo (PGDG); Ubuntu's
      # default repo lags behind.
      - 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

      # Point the CLI straight at the v17 binary so it can't pick up an older
      # pg_dump that may already be on the runner's PATH.
      - name: Tell backupdrill where pg_dump 17 lives
        run: echo "BACKUPDRILL_PG_DUMP=/usr/lib/postgresql/17/bin/pg_dump" >> "$GITHUB_ENV"

      - name: Set up Node 20
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Run backupdrill
        run: npx backupdrill backup
        env:
          # --- required ---
          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 }}
          # --- optional: destination bucket details ---
          BACKUPDRILL_S3_ENDPOINT: ${{ secrets.BACKUPDRILL_S3_ENDPOINT }}
          BACKUPDRILL_S3_REGION: ${{ secrets.BACKUPDRILL_S3_REGION }}
          BACKUPDRILL_PROJECT_NAME: ${{ secrets.BACKUPDRILL_PROJECT_NAME }}
          # --- optional: also back up Supabase Storage files (set all four) ---
          BACKUPDRILL_SUPABASE_STORAGE_ENDPOINT: ${{ secrets.BACKUPDRILL_SUPABASE_STORAGE_ENDPOINT }}
          BACKUPDRILL_SUPABASE_STORAGE_REGION: ${{ secrets.BACKUPDRILL_SUPABASE_STORAGE_REGION }}
          BACKUPDRILL_SUPABASE_STORAGE_ACCESS_KEY_ID: ${{ secrets.BACKUPDRILL_SUPABASE_STORAGE_ACCESS_KEY_ID }}
          BACKUPDRILL_SUPABASE_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.BACKUPDRILL_SUPABASE_STORAGE_SECRET_ACCESS_KEY }}

Two details earn their keep: concurrency prevents a slow run and the next scheduled run from backing up at the same time (which would double your egress), and the PGDG install matters because pg_dump’s major version must be at least your Supabase Postgres version — the runner’s default client can be older than your database.

Step 2 — get the Session Pooler connection string

In the Supabase dashboard, click the Connect button at the top of the page and pick Session pooler. Copy the URI and fill in your database password. The default postgres user in it works as-is — it owns your tables, so the dump passes row-level security. A least-privilege read-only role sounds better and usually is not: without bypassrls (which Supabase does not hand out), RLS silently filters what a non-owner role can dump.

Step 3 — create the destination bucket

Any S3-compatible bucket works. The bucket setup guide has exact click-paths for creating a bucket and a scoped access key pair on R2, S3, and B2. You want credentials scoped to this one bucket, not account-wide keys.

Step 4 — add the repository secrets

In your GitHub repo: Settings → Secrets and variables → Actions → New repository secret. Four are required:

  • BACKUPDRILL_DATABASE_URL — the Session Pooler string from step 2, password included.
  • BACKUPDRILL_S3_BUCKET — the bucket name from step 3.
  • BACKUPDRILL_S3_ACCESS_KEY_ID and BACKUPDRILL_S3_SECRET_ACCESS_KEY — the scoped credentials for that bucket.

The rest are optional, and unset secrets resolve to empty strings the CLI treats as “not provided” — so you can skip them freely. Set BACKUPDRILL_S3_ENDPOINT and BACKUPDRILL_S3_REGION for R2 or B2 (omit both for AWS S3; auto works as the R2 region). To include your Supabase Storage files in the same snapshot — which you should, because a database restore alone does not bring files back — set all four BACKUPDRILL_SUPABASE_STORAGE_* secrets with keys generated at Dashboard → Storage → Settings → S3 Access Keys.

Step 5 — run it once by hand, then check the bucket

Actions tab → scheduled-backup → Run workflow. When it goes green, look in the bucket: you should see a timestamped snapshot containing dump.pgcustom (plain pg_dump custom format — restorable with stock pg_restore, no proprietary anything), manifest.json with tables, estimated row counts, and sha256 checksums, and a storage/ tree if you configured the Storage secrets. If the run fails, the two usual suspects are a mistyped pooler string and bucket credentials that cannot write to the bucket — the log says which.

Egress: the Free plan gives you 5 GB per month

Every backup streams your data out of Supabase, and that counts against the Free plan’s 5 GB/month egress allowance — the same allowance your app uses. The arithmetic is friendly at weekly frequency: free-tier databases are capped at 500 MB, so even at the ceiling, weekly backups move roughly 2 GB/month. A daily schedule of that same ceiling-sized database would move roughly 15 GB/month — three times the entire allowance. Weekly is the free-tier frequency; keep workflow_dispatch for extra runs before migrations.

Going over is not a hard cutoff. Supabase notifies your billing email and applies a grace period; if usage stays over and nothing changes, Fair Use restrictions follow — projects can be paused, databases switched to read-only, API requests answered with 402s. Restrictions lift when you upgrade or when the quota resets at the start of the next billing cycle, and a used-up grace period does not renew — a later overage can be restricted without one. Treat the first notice as real. For your own numbers, npx backupdrill estimate projects monthly egress from your actual database size, and the CLI docs cover the paid-plan egress math.

Two ways DIY schedules quietly die

  • The 60-day rule in public repos. GitHub automatically disables scheduled workflows in a public repository after 60 days without repository activity — and a backup repo is exactly the kind of repo that goes quiet for 60 days. Use a private repository: the rule does not apply there, and your backup workflow’s existence is nobody’s business anyway.
  • Nobody reads the Actions tab. A failing scheduled job is only loud if something makes it loud. Check the tab when you touch the repo, and consider a notification step on failure — the failure mode this whole setup exists to prevent is the backup that stopped months ago without anyone noticing.

A green run is not a restorable backup

The workflow going green proves the job exited 0 — not that the dump restores. Truncated dumps and wrong-role dumps look identical to good backups as files in a bucket. Two honesty notes and one action: the dump covers the public schema by default, so Supabase-managed schemas (auth, storage metadata) are not in it; and the only way to know the backup works is to restore it somewhere disposable. The restore testing guide walks through the full manual drill, or run backupdrill drill locally — it pulls the latest snapshot from your bucket, restores it into a throwaway Docker Postgres, and verifies it against the manifest.

And if you would rather not own a cron, a YAML file, and the discipline to restore-test it, the hosted service runs this same engine on a schedule with restore drills and email reports built in.

FAQ

Does Supabase back up projects on the Free plan?

No. Automated daily backups start on the Pro plan; the Free plan does not include them. Supabase's own backup docs advise free-tier projects to export their data regularly with the CLI's db dump command and keep off-site backups — a scheduled GitHub Actions workflow is that advice turned into something that runs without you.

How often should I back up a free-tier Supabase project?

Weekly is the sensible default. Every backup counts against the Free plan's 5 GB/month egress allowance, and free-tier databases are capped at 500 MB — so weekly backups stay around 2 GB/month even at the size ceiling, leaving most of the allowance for your app, while a daily schedule of a ceiling-sized database would move roughly 15 GB/month, three times the allowance. Keep the workflow_dispatch trigger so you can fire an extra backup by hand before risky migrations.

What happens when a free-tier project gets paused?

Supabase pauses Free plan projects after 1 week of inactivity. A paused project can be restored from the dashboard for 90 days; after that it can no longer be restored in place — you can still download its backup and Storage files from the dashboard and migrate them to a new project. With your own scheduled backups in a bucket you control, a pause is an inconvenience instead of a data question.

Can I commit the database dump to the GitHub repo instead of a bucket?

You can — Supabase's officially documented DIY approach runs db dump on a schedule and commits the output to a private repo. It works, with trade-offs: the repo grows without bound because every dump lives in git history forever, anything sensitive in the data is very hard to purge from that history later, and restore tooling generally expects object storage. A bucket with lifecycle rules stays cheap, prunable, and boring — which is what you want from a backup target.

Sources

Facts and prices last verified July 11, 2026 against the sources above. Written by the team behind BackupDrill.

Want the schedule, the failure alerts, and the restore drills without owning the workflow? Open the console — the Free plan covers weekly backups for one project — or keep it DIY with the open-source CLI.