Supabase is great. I love Supabase. But the moment I saw “Backups” sitting behind a paid plan, my brain did what every broke founder's brain does: “I can build that.” “I can build that.” And yes, you can. On the free plan. With real automation. Without a server. Without waking up at 2 am to remember “oh yeah, I should dump my database.” This post shows how I set up automatic Supabase Postgres backups every 12 hours using GitHub Actions, plus all the annoying little errors you’ll probably hit (so you don’t have to). automatic Supabase Postgres backups every 12 hours GitHub Actions What we’re building A one-time setup that: Runs every 12 hours Connects to your Supabase Postgres database Creates a .dump backup file (compressed, proper format) Stores it in a private GitHub repo as versioned files Runs every 12 hours every 12 hours Connects to your Supabase Postgres database Creates a .dump backup file (compressed, proper format) .dump Stores it in a private GitHub repo as versioned files No paid Supabase plan. No third-party services. No VPS. Just GitHub doing the boring work. The only thing you must understand A database backup is like an umbrella. If you wait until it starts raining, you’re already wet. Also, “I’ll just recreate it” is a lie you tell yourself when you’ve never recreated it, Supabase free plan is basically a blackhole, if you get hacked, there’s no history to fallback to. Step 0: Create a private repo Create a private GitHub repository called something like: private myapp-db-backups supabase-backups notpayingforshiii-db-backups (if you’re me) myapp-db-backups myapp-db-backups supabase-backups supabase-backups notpayingforshiii-db-backups (if you’re me) notpayingforshiii-db-backups This repo will store your backup files. github repo dedicated to backups Step 1: Get the correct Supabase connection string (this part matters) Go to your Supabase project: Settings → Database → Connection string Settings → Database → Connection string Now here’s the trap: Use Session Pooler (not Direct) If you are on the free plan, Supabase often shows: Direct connection: Not IPv4 compatible Session pooler: IPv4 compatible Direct connection: Not IPv4 compatible Session pooler: IPv4 compatible GitHub runners are typically IPv4. If you use Direct, your workflow will fail and you’ll think the universe hates you. It doesn’t. It just hates IPv6 sometimes. Set: Method: Session pooler Type: URI Method: Session pooler Method: Session pooler Type: URI Type: URI You’ll get a connection string like: postgresql://postgres.<your-project-ref>:YOUR_PASSWORD@aws-1-<region>.pooler.supabase.com:5432/postgres postgresql://postgres.<your-project-ref>:YOUR_PASSWORD@aws-1-<region>.pooler.supabase.com:5432/postgres Important: Replace the [YOUR-PASSWORD] placeholder with your Supabase password, if you don’t remember it, there’s a db password reset link on that connection popup. Replace the [YOUR-PASSWORD] placeholder with your Supabase password, if you don’t remember it, there’s a db password reset link on that connection popup. Step 2: Add the connection string as a GitHub secret In your GitHub repo: Settings → Secrets and variables → Actions → New repository secret Settings → Secrets and variables → Actions → New repository secret Create a secret: Name: SUPABASE_DB_URL Value: paste the full Session Pooler connection string Name: SUPABASE_DB_URL Name: SUPABASE_DB_URL Value: paste the full Session Pooler connection string Value: Do not add extra spaces. Do not add a newline. Don’t get cute. Add Your Secret Step 3: Add the GitHub Actions workflow file In your repo, create this file: .github/workflows/backup.yml .github/workflows/backup.yml Yes, including the dots. GitHub will create the folders automatically. Now paste this full workflow: name: Supabase Backupon: schedule: - cron: "0 */12 * * *" workflow_dispatch:jobs: backup: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install matching PostgreSQL client env: DATABASE_URL: ${{ secrets.SUPABASE_DB_URL }} run: | sudo apt-get update sudo apt-get install -y wget ca-certificates lsb-release postgresql-client SERVER_VERSION=$(psql "$DATABASE_URL" -tAc "SHOW server_version_num;" | cut -c1-2) echo "SERVER_VERSION=$SERVER_VERSION" >> $GITHUB_ENV wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - echo "deb http://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-$SERVER_VERSION - name: Run pg_dump env: DATABASE_URL: ${{ secrets.SUPABASE_DB_URL }} run: | mkdir -p backups /usr/lib/postgresql/$SERVER_VERSION/bin/pg_dump "$DATABASE_URL" \ --format=custom \ --file=backups/supabase_$(date +%F_%H-%M).dump - name: Commit backup run: | git config user.name "supabase-backup-bot" git config user.email "backup@github.com" git add backups git commit -m "Automated Supabase backup" || echo "No changes" git pushWhat this does: name: Supabase Backupon: schedule: - cron: "0 */12 * * *" workflow_dispatch:jobs: backup: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install matching PostgreSQL client env: DATABASE_URL: ${{ secrets.SUPABASE_DB_URL }} run: | sudo apt-get update sudo apt-get install -y wget ca-certificates lsb-release postgresql-client SERVER_VERSION=$(psql "$DATABASE_URL" -tAc "SHOW server_version_num;" | cut -c1-2) echo "SERVER_VERSION=$SERVER_VERSION" >> $GITHUB_ENV wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - echo "deb http://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-$SERVER_VERSION - name: Run pg_dump env: DATABASE_URL: ${{ secrets.SUPABASE_DB_URL }} run: | mkdir -p backups /usr/lib/postgresql/$SERVER_VERSION/bin/pg_dump "$DATABASE_URL" \ --format=custom \ --file=backups/supabase_$(date +%F_%H-%M).dump - name: Commit backup run: | git config user.name "supabase-backup-bot" git config user.email "backup@github.com" git add backups git commit -m "Automated Supabase backup" || echo "No changes" git pushWhat this does: Detects the Supabase DB Postgres version Installs the matching pg_dump Creates a .dump file Commits it into your repo Detects the Supabase DB Postgres version Installs the matching pg_dump pg_dump Creates a .dump file .dump Commits it into your repo This version is future-proof: if Supabase upgrades Postgres later, the workflow adapts. future-proof Step 4: Give GitHub Actions permission to push Do this before you run the workflow, because the backup will fail when it tries to save (commit + push) the database dump back into your repo. before you run the workflow save (commit + push) the database dump Here’s how to enable it: Repo → Settings → Actions → General → Workflow permissions Select: ✅ Read and write permissions Click Save. Save Step 5: Run it manually once Now, test it out to make sure it works before you rest easy. Go to your repo: Actions → Supabase Backup → Run workflow Actions → Supabase Backup → Run workflow If it succeeds: You’ll see a new folder: backups/ A file like: supabase_2026-01-24_00-15.dump You’ll see a new folder: backups/ backups/ A file like: supabase_2026-01-24_00-15.dump supabase_2026-01-24_00-15.dump What about storage buckets? This backup is for your Postgres database. Postgres database It does not include: Supabase Storage files (images, videos, uploads) Edge function code Logs Supabase Storage files (images, videos, uploads) Edge function code Logs If you need Storage backups too, you’ll want a separate process (S3-compatible sync or a script — could extend this to do all this, but i only needed the database in my case). Bonus: Keep your repo from growing forever (optional) If you don’t want infinite backups piling up, you can delete older files automatically (keep last 30, for example). I’m not adding it here to keep the guide clean, but it’s easy to include. Final thoughts You don’t need a paid plan to have grown-up backups. You just need: A GitHub repo One secret One workflow One permission toggle A GitHub repo One secret One workflow One permission toggle And now your database has backups every 12 hours while you sleep, ship, and pretend you’re not responsible for production. If you found this helpful, feel free to steal it. That’s the culture.