---
name: "Supabase Backup"
description: "Back up a Supabase or Postgres database to your own S3, R2 or B2 bucket with the backupdrill CLI, and optionally test-restore the backup in a throwaway Postgres."
author: "backupdrill"
repo: "supabase-backup-buildkite-plugin"
stars: 0
official: false
---

# 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](https://backupdrill.com/docs/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.

```yaml
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](https://buildkite.com/docs/pipelines/configure/workflows/scheduled-builds) for a pipeline with this step, for example `0 3 * * 0` for Sundays at 03:00 UTC:

```yaml
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](https://buildkite.com/docs/pipelines/security/secrets/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](https://backupdrill.com/docs/cli) 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 `node` and `npm` on the `PATH`. 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 `.npmrc` don't apply to that install, so a repository can't swap in another registry.
- `pg_dump` with a major version at least as new as the server. Supabase runs Postgres 15 to 17, so `pg_dump` 17 covers every project. The CLI checks the version and refuses a mismatch. To use a binary that isn't first on the `PATH`, set `BACKUPDRILL_PG_DUMP`.
- For `drill: true`: Docker, and `pg_restore` from the same Postgres version as `pg_dump` (`BACKUPDRILL_PG_RESTORE` to point at it). The drill publishes the sandbox port on `127.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 your `meta-data-key`), for later steps: `buildkite-agent meta-data get backupdrill-snapshot`.
- The backup in your bucket:

```text
<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](https://backupdrill.com/docs/cli#egress-cost-read-before-scheduling-daily-backups) before you schedule it daily.

## Developing

```shell
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`:

```shell
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