Supabase Backup Buildkite Plugin
Backs up a Supabase (or any Postgres) database to a bucket you own: AWS S3, Cloudflare R2, Backblaze B2, MinIO or anything else that speaks the S3 API. With drill: true it then restores the new backup into a throwaway Postgres container on the agent and checks that it came back.
The step runs the backupdrill CLI, an open-source (MIT) Supabase backup tool. The dump goes from your database through the agent straight to your bucket. Next to it the CLI writes a manifest.json (tables, estimated row counts, sizes, SHA-256 checksums) and a RECOVERY.md with restore steps.
Example
Back up before a deploy runs its migrations. If the backup fails, the steps after the wait don’t run.
steps:
- label: ":floppy_disk: Back up Supabase"
secrets:
- BACKUPDRILL_DATABASE_URL
- BACKUPDRILL_S3_ACCESS_KEY_ID
- BACKUPDRILL_S3_SECRET_ACCESS_KEY
plugins:
- backupdrill/supabase-backup#v1.0.0:
s3-endpoint: https://<account-id>.r2.cloudflarestorage.com
s3-bucket: my-db-backups
project-name: my-app
- wait
- label: ":rocket: Migrate"
command: ./migrate.sh
Weekly backup with a restore drill
Create a scheduled build for a pipeline with this step, for example 0 3 * * 0 for Sundays at 03:00 UTC:
steps:
- label: ":floppy_disk: Weekly Supabase backup and restore drill"
secrets:
- BACKUPDRILL_DATABASE_URL
- BACKUPDRILL_S3_ACCESS_KEY_ID
- BACKUPDRILL_S3_SECRET_ACCESS_KEY
plugins:
- backupdrill/supabase-backup#v1.0.0:
s3-region: us-east-1
s3-bucket: my-db-backups
drill: true
The drill downloads the dump, checks its checksum, restores it into a postgres container with pg_restore, and compares the restored tables with the manifest: every table is there, and tables that had rows still have rows. The step fails if any check fails.
Secrets
Credentials never go in the plugin options, because those end up in the pipeline YAML. The plugin reads them from the job environment. The examples above use Buildkite secrets with the same names; an agent environment hook or your own secrets plugin works just as well.
| Variable | Required | What it is |
|---|---|---|
BACKUPDRILL_DATABASE_URL | yes | Connection string of the database, password included. For Supabase, open the project, click Connect and copy the Session pooler string. The default postgres user works. |
BACKUPDRILL_S3_ACCESS_KEY_ID | yes | Access key for the backup bucket. |
BACKUPDRILL_S3_SECRET_ACCESS_KEY | yes | Secret key for the backup bucket. |
BACKUPDRILL_SUPABASE_STORAGE_ENDPOINT, _REGION, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY | no | Set all four to copy Supabase Storage files as well. Create the keys under Storage > Settings > S3 Access Keys. |
BACKUPDRILL_PING_URL | no | A Healthchecks.io ping URL. The backup and the drill report start, success and failure to it. |
Every other BACKUPDRILL_* variable the CLI knows works too; the CLI docs list them. DATABASE_URL is ignored on purpose: in CI it usually points at the app’s test database, and the CLI would otherwise fall back to it. A backupdrill.config.json in the checked-out repository is ignored for the same reason.
The drill only reads the bucket. It runs without the database URL and the Storage keys.
Options
All options are optional and safe to commit. An option wins over the matching BACKUPDRILL_* variable.
| Option | Default | Description |
|---|---|---|
s3-bucket | BACKUPDRILL_S3_BUCKET | Bucket the backup is written to. Required one way or the other. |
s3-endpoint | none | Endpoint of an S3-compatible service (R2, B2, MinIO). Leave it out for AWS S3. |
s3-region | auto | Bucket region. AWS S3 needs the real one. |
s3-prefix | backupdrill | Key prefix inside the bucket. |
project-name | supabase | Name used in the object keys and the manifest. |
schemas | public | Schemas to dump, as a list or a comma-separated string. An empty entry in the list is an error. |
drill | false | Restore the new backup into a throwaway Postgres and check it. |
meta-data-key | backupdrill-snapshot | Build meta-data key the snapshot name is stored under. Give each step its own key when one build backs up several projects. |
backupdrill-version | 2 | CLI version from npm: 2 means the newest 2.x release, 2.8.1 pins one. |
Agent requirements
- Node.js 20 or newer, with
nodeandnpmon thePATH. Each run installs the CLI from the public npm registry (https://registry.npmjs.org/) into a temporary directory, without install scripts. npm settings from the repository being built or from the agent user’s.npmrcdon’t apply to that install, so a repository can’t swap in another registry. pg_dumpwith a major version at least as new as the server. Supabase runs Postgres 15 to 17, sopg_dump17 covers every project. The CLI checks the version and refuses a mismatch. To use a binary that isn’t first on thePATH, setBACKUPDRILL_PG_DUMP.- For
drill: true: Docker, andpg_restorefrom the same Postgres version aspg_dump(BACKUPDRILL_PG_RESTOREto point at it). The drill publishes the sandbox port on127.0.0.1, so the job has to run where that address reaches the Docker host. A job running directly on the agent host does; one inside a container needs host networking and the Docker socket.
What the step leaves behind
- An annotation with the result, for example: Backed up 2 tables (12.8 KiB dump) to snapshot 2026-10-31T08-17-48-954Z with backupdrill 2.8.1. Restore drill passed: 2 tables and 1,300 rows restored in 0.1s.
- The snapshot name in the build meta-data under
backupdrill-snapshot(or yourmeta-data-key), for later steps:buildkite-agent meta-data get backupdrill-snapshot. - The backup in your bucket:
<s3-prefix>/<project-name>/<snapshot>/
dump.pgcustom pg_dump custom format
storage/<bucket>/<key> only with the Storage keys set
RECOVERY.md
manifest.json written last, so a snapshot without it is incomplete
The manifest lists every table name and Storage file path. The CLI prints it on standard output, and the plugin keeps that out of the build log.
To get the data back, backupdrill restore restores a snapshot into a new Supabase project (database and Storage files), or you can load dump.pgcustom with pg_restore. RECOVERY.md in each snapshot has the commands.
Cost
Each backup dumps the schemas you chose in full, and that traffic counts as egress on a Supabase bill. Read the egress notes in the CLI docs before you schedule it daily.
Developing
docker compose run --rm tests # bats tests, with npm, the CLI and buildkite-agent stubbed
docker compose run --rm lint # Buildkite plugin linter
tests/e2e/run.sh runs the hook for real: it starts Postgres 17 and a SeaweedFS S3 store in Docker, loads a small test database, backs it up and drills the snapshot. It needs the same tools as an agent (see above), or run it in the image under tests/e2e:
docker build -t supabase-backup-e2e tests/e2e
docker run --rm --network host -v /var/run/docker.sock:/var/run/docker.sock \
-v "$PWD:/plugin:ro" -w /plugin supabase-backup-e2e bash tests/e2e/run.sh
License
MIT