Backups
Free Supabase backup with a GitHub Action, and the catch
A Supabase backup GitHub Action costs nothing and suits a lot of apps. The workflow, the connection string that works on GitHub, and the egress each run uses.

In short
- A Supabase backup GitHub Action runs the Supabase CLI on a schedule and commits the dump to a private repository. It costs nothing, and for a lot of apps it is the right answer.
- Every run downloads your whole database, and Supabase counts that as egress. The free plan includes 5 GB a month for the whole organization, and a daily dump of a database near the 500 MB limit uses about three times that.
- Supabase's own example connects with a string GitHub's runners cannot reach. Put the Session pooler string in the secret, and restore one copy before you trust the rest.
Your Supabase project is on the free plan, so nothing is being copied anywhere, or it is on Pro and every copy it takes lives inside the account you are worried about losing. In a thread about exactly that, somebody said the answer is a Supabase backup GitHub Action: one small file in a repository that dumps your database every night and costs nothing.
They were right. Supabase publishes the workflow itself, and for a lot of apps it is the thing to set up this week. Here is the part those threads leave out: it costs no money, and it still has a price. Every run downloads your whole database, and Supabase meters that download.
Think of it as the data on a phone contract. Your plan comes with a monthly allowance, everything your app does draws on it, and a backup is a large download on the same plan. On the free plan, a nightly copy of a database near its size limit uses the whole month's allowance in about ten days. That, and a connection string GitHub cannot reach, are the two things between you and a working backup, and neither is on Supabase's page.
Can you back up Supabase with a GitHub Action for free?
Yes, and Supabase documents how. Its page on automated backups using GitHub Actions gives a workflow that installs the Supabase CLI, dumps your roles, your schema and your data into three files, and commits them back into the repository on a schedule.
GitHub does not charge for it either, within limits. A private repository on GitHub Free gets 2,000 Actions minutes a month, and a nightly dump of a small database uses a small part of them.
What you get is a copy that leaves Supabase every night and lands on another company's servers. That is the one thing Supabase's own backups cannot give you, because the daily copies on a paid plan stay inside the account they protect.
It is the right answer when three things are true. Your app already lives in a GitHub repository, which it does if you switched on GitHub sync in Lovable or a builder like it. Editing a YAML file does not worry you. And your database is small next to your plan's egress allowance. You know the first two already. The third is arithmetic, and it has its own section below.
The workflow, with the schedule and the secret
Save this as .github/workflows/supabase-backup.yml in a private repository
that holds nothing else:
name: supabase-backup
on:
schedule:
- cron: '17 3 * * *' # every day at 03:17 UTC
workflow_dispatch:
jobs:
backup:
runs-on: ubuntu-latest
permissions:
contents: write
env:
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
steps:
- uses: actions/checkout@v7
- uses: supabase/setup-cli@v3
with:
version: latest
- name: Back up roles
run: supabase db dump --db-url "$SUPABASE_DB_URL" -f roles.sql --role-only
- name: Back up schema
run: supabase db dump --db-url "$SUPABASE_DB_URL" -f schema.sql
- name: Back up data
run: >
supabase db dump --db-url "$SUPABASE_DB_URL" -f data.sql --use-copy --data-only
-x "storage.buckets_vectors" -x "storage.vector_indexes"
- uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: Supabase backup
It is Supabase's workflow with three changes.
- It runs on the schedule and when you press the button, and at no other
time. Supabase's version also runs on every push and every pull request to
main. Each run is a full download of your database, so in a repository that also holds your app, every change you shipped would spend a backup's worth of allowance. - It runs at 03:17, off the hour. GitHub says scheduled runs
can be delayed at the start of every hour,
and that under enough load some queued jobs are dropped. Supabase's example
uses
0 0 * * *, which is on the hour. Times are UTC unless you add atimezone, and any minute other than0will do. - The data line matches Supabase's current
backup and restore guide,
which adds two
-xexclusions the workflow page does not have. The files you keep are then the ones Supabase's restore instructions are written for.
Then the secret. In the repository, open Settings, then Secrets and variables,
then Actions, and add a repository secret called SUPABASE_DB_URL. What goes
into it decides whether any of this works, so it gets the next section to
itself.
Once it is saved, run the workflow by hand from the Actions tab, so the first
run happens while you are watching. workflow_dispatch is the line that gives
you the Run workflow button.
Which connection string goes in the secret?
The Session pooler string, from the Connect button at the top of your Supabase project. Use it even though Supabase's workflow page shows the direct connection in its example.
The reason is IPv6, the newer kind of internet address. Supabase's direct
connection, the host that begins db. and ends supabase.co,
uses IPv6 by default,
and the same page lists GitHub Actions among the services that only accept
IPv4. The runner is handed an address it has no route to, and the first dump
fails before a byte is written. The error says the host name could not be
translated, or that the network is unreachable, often with a long address full
of colons printed beside it. That address is the IPv6 one. It reads like a
wrong password or a database that is down, and the real cause is a network the
runner is not on.
Supabase's backup and restore guide says to use the Session pooler string by
default, and the direct one only if your network supports IPv6 or you pay for
the IPv4 add-on. You can recognise the pooler string by three things: its user
name is postgres. followed by your project's reference, its host ends in
pooler.supabase.com, and its port is 5432.
The password in it is your database password, the one set when the project was created, and not an API key. If nobody wrote it down, Supabase lets you reset it under Database Settings; anything else that connects with the old one stops working until you give it the new one.
That string can read and change every row in your database. It lives in the secret and nowhere else: never in the workflow file, never in a commit message, and never pasted into an AI assistant along with the error you were asking about.
Does a Supabase backup count against my egress?
Yes, all of it. Supabase counts as egress the data any of its services sends out, and a dump through the pooler is filed as Shared Pooler Egress inside the same allowance as your API traffic, your file downloads and everything else your app serves.
Back to the phone contract. The allowance resets each billing month, every service your app uses draws on it, and a backup is one more large download. It is also a family plan: Supabase applies the quota to your whole organization, so every project in it draws on the one allowance.
On 26 September 2026, Supabase's pricing page gave the free plan 5 GB of egress a month and a 500 MB limit on each project's database, and the Pro plan 250 GB of egress. There is a second 5 GB on the free plan for cached egress, which covers files served from Supabase's CDN, and a backup never touches it. The other free plan limits, and what each one does when you cross it, have an article of their own.
Going past the allowance on the free plan does not produce a bill. Supabase notifies you and gives you a grace period, and if you keep going over, it restricts every project in the organization. It says that can mean API requests answered with a 402 error, a database switched to read-only, or projects paused. For an app with customers, that is an outage your backup started.
This is true of every backup that leaves Supabase, including the one we sell. A copy held outside the account has to be downloaded to get there.
How often should the backup run?
As often as your allowance pays for, and that comes down to one multiplication: your database size, times the runs in a month.
Supabase shows your database size on the Database report, under Observability in your project. A dump is not exactly that size. It leaves out indexes, which are rebuilt from their definitions, and it writes every value out as text. It is close enough to plan with, and it is the number you can look up.
| Database size | Daily (30 runs) | Twice a week | Weekly |
|---|---|---|---|
| 50 MB | 1.5 GB | 0.4 GB | 0.2 GB |
| 100 MB | 3 GB | 0.9 GB | 0.4 GB |
| 250 MB | 7.5 GB | 2.2 GB | 1.1 GB |
| 500 MB | 15 GB | 4.3 GB | 2.2 GB |
A month is about 4.3 weeks, which is where the last two columns come from.
On the free plan, the bottom two daily figures spend more than the whole month's allowance before your app has answered a single request. A daily schedule uses the full 5 GB at about 160 MB, and your app's own traffic comes out of the same allowance, so read last month's egress on your organization's Usage page before you pick. Weekly fits at every size the free plan allows.
The cron line is the only thing you change:
17 3 * * * every day at 03:17 UTC
17 3 * * 1,4 Mondays and Thursdays
17 3 * * 0 Sundays
On Pro the arithmetic mostly stops mattering. 250 GB a month pays for a daily dump of one project up to about 8 GB, which is the disk space the Pro plan includes per project.
Why does my workflow fail with a pg_dump version error?
Because the pg_dump it ran is older than your database. That only happens to
a workflow that calls pg_dump directly rather than through the Supabase CLI.
The CLI brings its own pg_dump, which is one reason the workflow above uses
it.
GitHub's ubuntu-latest runner comes with
PostgreSQL 16 installed.
Supabase projects can run Postgres 17, and Postgres documents that pg_dumpwill not dump from a server newer than its own major version,
refusing rather than risking a bad file. The run stops with:
pg_dump: error: aborting because of server version mismatch
pg_dump: detail: server version: 17.…; pg_dump version: 16.…
Your project's version is under Project Settings, then General, if you want to confirm it. Nothing about your credentials is wrong.
The fix is two steps. Add PostgreSQL's own package repository so the runner can install the version 17 client, then call that client by its full path:
- name: Install the 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: Back up
run: /usr/lib/postgresql/17/bin/pg_dump "$SUPABASE_DB_URL" …
Keep the arguments you already had after the connection string. The full path
matters: on Ubuntu, plain pg_dump is a wrapper that picks a version for you,
and the runner already has a PostgreSQL 16 installation for it to pick.
The CLI's own pg_dump has a version as well, and it is not read from your
project. With no supabase/config.toml beside the workflow, which is the case
in a repository that holds nothing else, the CLI
picks pg_dump 17.
On a project still running Postgres 15, the data.sql it writes then carries
SET transaction_timeout = 0, a setting Postgres 15 does not have, and the
replay stops on that line in any Postgres 15 database, the project the file came
from included. We reproduced it on 4 October 2026 with pg_dump 17.11 and
Postgres 15.19. If your project is on 15, add one line to the job's env block
so the CLI runs the matching pg_dump:
env:
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
SUPABASE_DB_MAJOR_VERSION: '15'
The backup itself runs without an error either way, so without that line the mismatch only shows on the day the file is replayed.
Can I commit the backup to my repository?
To a private one, yes. That is what Supabase's workflow does, and its page says twice that you should never back up your data to a public repository.
Three things about a repository as a home for a database, none of them on that page:
- Everyone who can read the repository can read your users.
data.sqlholds every row: email addresses, names, whatever your tables keep, and your accounts as well. That is why the workflow gets a repository of its own with nobody else on it, rather than the one your app's code lives in, which your builder may sync and a freelancer may one day be invited to. - Every night's copy stays in the history. Git keeps every version of every file. When a customer asks you to delete their account, their row is still in every earlier commit of that repository until you rewrite its history.
- It stops working at 100 MiB. GitHub warns about files over 50 MiB and
blocks files over 100 MiB,
and the same page says Git is not designed to handle large SQL files. The
night
data.sqlcrosses that line, the commit step fails, and every run after it fails the same way.
As you get close, the same workflow can upload the three files to a storage bucket you own instead of committing them. Or it is the point where doing this yourself has stopped being cheap, which is where the last section starts.
What does the workflow not copy?
Your uploaded files. Supabase Storage keeps each file outside the database and a row describing it inside, so the dump carries the row and not the picture. Backing up Storage is a job of its own, on every route there is.
Your users are in it. The data dump walks the auth schema where your accounts
live, and searching data.sql for auth.users shows them. The project around
the database is not: Edge Functions, auth provider settings, API keys and
secrets are configuration rather than data, and
the list of what neither copy holds is
the one to read before you need it.
Does a green run mean the backup worked?
It means the job finished without an error, which is a smaller claim. Three quite different results end in the same green tick:
- A good dump of the right project.
- A dump of the wrong project, because the secret holds the string for a staging copy.
- A dump with no rows in it, because somebody edited the data line and
--data-onlywent missing, which turns it back into the schema dump.
One result leaves no mark at all. A scheduled run GitHub drops under load does not show up as a failure, because there is no run to fail. And when a run does fail, GitHub sends the notice to the person who last edited the cron line. If a freelancer set this up for you, the email about your backup goes to them.
A restore is the only thing that tells them apart. Replay the three files into a throwaway Supabase project or a local one, compare the row counts of the tables you care about with the live ones, and sign in as a real user. How to restore a Supabase backup has the commands, in the order Supabase gives them. Do it once now, and again every time you change the workflow.
You can also have every run do the restore and the counting for you. We published a version of this workflow as an open-source GitHub Action, Reeve-page/supabase-backup-action. It takes the same three dumps, keeps the copy as a workflow artifact or in an S3 or R2 bucket, then replays it into a throwaway Supabase database on the runner and compares the row count of every table with the file. The run goes green only when every table came back with the rows the file holds:
- uses: Reeve-page/supabase-backup-action@v1
with:
db-url: ${{ secrets.SUPABASE_DB_URL }}
It is free and MIT-licensed. Every run still downloads the whole database, so the egress arithmetic above applies to it unchanged, and the sign-in is still yours to test.
When should I stop doing this myself?
When the arithmetic stops working, when the file outgrows the repository, or when nobody is restoring the copies. Any one of them is enough.
- Your database has outgrown the free allowance. Pro raises egress to 250 GB and adds Supabase's own daily backups, kept for seven days, which restore with a button. Keep the workflow running beside them, because those copies live inside the account.
data.sqlis heading for 100 MiB. Moving it to a bucket is more YAML, and more for somebody to keep working.- Nobody has restored a copy. The workflow will go on writing files whether or not they open.
- Your users upload things. The workflow does not reach them, and the job that does is a second one to keep alive.
Where Reeve Care fits
Care takes the copy on a schedule, keeps it outside your Supabase account, and checks every copy before it counts.
- Daily on Care, and more often on the plans above it, with nothing in your repository and no connection string sitting in CI.
- Read back and counted. Every copy is opened and counted against what went in, and the date on your dashboard is the last copy that passed that check, never the last run.
- Your accounts are in it, and your uploaded files come too once you connect a Storage credential.
- Restoring is a button, and a copy of the current state is taken before anything is replaced.
- Any copy downloads as a zip holding
schema.sql,data.sqlandroles.sql, the same three files this workflow makes, plus a count of the rows in every table.
Care keeps a copy of your Supabase database. The copy, the check and the restore are drawn step by step on the Supabase backups page, and what each plan includes is on the pricing page.
What to do this week
What to do
- Look up your database size and last month's egress, and pick the schedule from the table before you write the cron line.
- Create a private repository that holds nothing but the workflow, and put the Session pooler string in its
SUPABASE_DB_URLsecret. - If you started from Supabase's example, delete the push and pull request triggers and move the cron off the hour.
- Run it once by hand from the Actions tab, then search
data.sqlforauth.usersand for a table you know has rows. - Restore one copy into a throwaway project and sign in as a real user.
- Copy your Storage files on a job of their own.
Before you close this tab, open your organization's Usage page in Supabase and read last month's egress. That figure, next to your database size, picks your cron line. And if you are still weighing the free route against the paid ones, the four kinds of Supabase backup tool are compared side by side.
FAQ
Can I back up Supabase for free?
Yes. Supabase documents a GitHub Actions workflow that installs its CLI, dumps your roles, schema and data into three files on a schedule, and commits them to the repository. A private repository on GitHub Free gets 2,000 Actions minutes a month, and a nightly dump of a small database uses a small part of them. What the backup does spend is your Supabase egress allowance, because every run downloads the whole database.
How often should a Supabase backup cron run?
As often as your egress allowance can pay for. Multiply your database size by the runs in a month: a 100 MB database dumped daily moves about 3 GB, and a 500 MB one about 15 GB. The free plan includes 5 GB for the whole organization, shared with your app's own traffic. Put the job on a minute that is not on the hour, because GitHub says scheduled runs at the start of an hour can be delayed, and some dropped.
Does a Supabase backup count against my egress?
Yes. Supabase counts as egress the data any of its services sends out, and a dump through the connection pooler is filed as Shared Pooler Egress inside the same allowance as your API traffic. The allowance belongs to the whole organization, not to one project. On the free plan, going over it repeatedly ends in restrictions on every project in the organization.
Why does my backup workflow fail on the first run?
Two failures belong to this setup. The first is the connection string: Supabase's direct connection uses IPv6 unless you pay for the IPv4 add-on, and Supabase lists GitHub Actions among the services that reach only IPv4, so use the Session pooler string. The second hits workflows that call pg_dump directly: the runner's PostgreSQL 16 client refuses to dump a Postgres 17 project and stops with aborting because of server version mismatch.
Can I commit my Supabase backup to my GitHub repository?
To a private one, yes, which is what Supabase's workflow does. Never to a public one, and Supabase says so twice on the same page. Keep it in a repository of its own that nobody else can read, because the data file holds your users' email addresses. Expect to move it as the database grows: GitHub warns about files over 50 MiB and refuses files over 100 MiB.
How do I know the backup file is any good?
Restore it. A green run means the job finished without an error, and a dump of the wrong project or a dump with no rows in it finish the same way. Replay the three files into a throwaway Supabase project or a local one, compare the row counts of the tables you care about, and sign in as a real user. That is the check that tells you whether the file opens.