Back up Supabase to Cloudflare R2
Cloudflare R2 is the destination we recommend first for Supabase backups, and the reason is one line on its pricing page: egress is free. Backups are write-once, read-rarely — until the day you read every byte back, in a restore or a restore test. This guide is the complete R2 setup: bucket and credentials with exact click-paths, the endpoint and path-style details that trip up S3 tools, working CLI config, and a weekly GitHub Actions schedule.
Why R2 for Supabase backups
Three numbers, checked against Cloudflare’s pricing page (July 2026):
- Zero egress fees. Reading data out of R2 costs nothing in bandwidth — on any other major provider, the restore path has a meter on it. This matters more for backups than for most workloads, because the whole point of a backup is the day you pull all of it back out: a real recovery, or the restore drills that should be happening on a schedule. With R2, drilling weekly adds nothing to the storage bill.
- Storage at $0.015/GB-month. Standard class, no tiers to reason about. A 10 GB project keeping ten snapshots — 100 GB — costs about $1.50/month. Per-operation fees exist ($4.50 per million writes, $0.36 per million reads on Standard) but a scheduled backup makes a handful of requests per run, so they round to zero at backup scale.
- A free tier that covers small projects entirely. 10 GB-month of Standard storage, 1 million write operations, and 10 million reads per month are free. A free-tier Supabase database is capped at 500 MB — several retained snapshots of one fit inside R2’s free tier with room to spare.
The honest counterweight: if your infrastructure, IAM, and billing already live on AWS, adding a Cloudflare account just for backups has a real cost in moving parts — the S3 version of this guide covers that path, and if raw storage price matters most, Backblaze B2 is the third option. And R2’s zero egress does not touch the other side of the pipe: a backup still pulls data out of Supabase, which counts against your Supabase plan’s egress allowance — the GitHub Actions guide walks through that math.
Step 1 — create the bucket
Cloudflare dashboard → R2 Object Storage → Create bucket. Any name works, e.g. my-backups. Two creation-time options are worth a decision now rather than later:
- Location hint. An optional field that tells R2 where you expect the data to be accessed from — six values (Western/Eastern North America, Western/Eastern Europe, Asia-Pacific, Oceania). It is explicitly best-effort, not a guarantee, and it is only honored the first time a bucket with a given name is created — deleting and recreating the bucket under the same name keeps the original placement. Pick the region where you will run restores and drills.
- Jurisdiction. If your backups must stay in the EU for GDPR reasons, create the bucket under the EU jurisdiction instead of relying on a location hint — a jurisdiction is a guarantee that objects are stored within it, and unlike a hint it is binding. It cannot be changed after creation, and it changes your S3 endpoint (see below).
Step 2 — create a scoped API token
R2 overview → Manage R2 API Tokens → Create API token. Permissions: Object Read & Write, scoped to your bucket — not account-wide. Scoped credentials are the difference between a leaked key exposing one backup bucket and exposing everything on the account.
On the success page, skip the “Token value” at the top — that is Cloudflare’s own API token format, not what S3 tools speak. Scroll to the credentials for S3 clients at the bottom and copy both values: Access Key ID and Secret Access Key. The secret is shown only once.
Step 3 — the endpoint, region, and path-style detail
The same success page shows your S3 API endpoint. Three values every S3-compatible tool needs for R2:
Endpoint: https://<account-id>.r2.cloudflarestorage.com
Region: auto
Path-style: works out of the box (the CLI defaults to it when an endpoint is set)On addressing: R2 accepts both S3 styles — path-style (bucket in the URL path) and virtual-hosted (bucket as a subdomain). The backupdrill CLI defaults BACKUPDRILL_S3_FORCE_PATH_STYLE to true whenever an endpoint is set, which works on R2 without touching anything — if you point other tooling at the same bucket and it fails on bucket resolution, its path-style switch is the first thing to check. EU jurisdiction buckets use https://<account-id>.eu.r2.cloudflarestorage.com instead, and are only reachable through it.
Step 4 — point the CLI at R2 and run the first backup
The open-source backupdrill CLI (MIT, github.com/backupdrill/cli) dumps the database with pg_dump, optionally copies your Storage files, and writes a checksummed manifest.json next to the dump. Configure it with env vars:
export BACKUPDRILL_DATABASE_URL="postgresql://postgres.<ref>:<pw>@aws-0-<region>.pooler.supabase.com:5432/postgres"
export BACKUPDRILL_PROJECT_NAME="my-app"
export BACKUPDRILL_S3_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
export BACKUPDRILL_S3_REGION="auto"
export BACKUPDRILL_S3_BUCKET="my-backups"
export BACKUPDRILL_S3_ACCESS_KEY_ID="…"
export BACKUPDRILL_S3_SECRET_ACCESS_KEY="…"
npx backupdrill backupThe connection string is the Session Pooler URI from the Connect button at the top of the Supabase dashboard — the default postgres user in it works as-is. Or keep the non-secret parts in a config file (backupdrill.config.json, gitignored in the CLI repo’s template) and the secrets in env vars:
{
"databaseUrl": "postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres",
"projectName": "my-app",
"schemas": ["public"],
"storage": {
"endpoint": "https://<account-id>.r2.cloudflarestorage.com",
"region": "auto",
"bucket": "my-backups",
"accessKeyId": "PUT-IN-ENV-OR-HERE",
"secretAccessKey": "PUT-IN-ENV-OR-HERE",
"prefix": "backupdrill",
"forcePathStyle": true
}
}npx backupdrill backup --config backupdrill.config.jsonTo capture Supabase Storage files in the same snapshot — which you want, because a database restore alone does not bring files back — also set the four BACKUPDRILL_SUPABASE_STORAGE_* variables with keys from Dashboard → Storage → Settings → S3 Access Keys. When the run finishes, the bucket holds a timestamped snapshot: dump.pgcustom, manifest.json, and a storage/ tree if Storage is configured.
Step 5 — schedule it with GitHub Actions
A backup you run by hand is a backup that stops the week you get busy. The GitHub Actions guide has the complete workflow file and the egress arithmetic; the R2 specifics are just the repository secrets. Required: BACKUPDRILL_DATABASE_URL, BACKUPDRILL_S3_BUCKET, BACKUPDRILL_S3_ACCESS_KEY_ID, BACKUPDRILL_S3_SECRET_ACCESS_KEY. For R2, also set BACKUPDRILL_S3_ENDPOINT to your https://<account-id>.r2.cloudflarestorage.com endpoint and BACKUPDRILL_S3_REGION to auto.
Housekeeping: prune old snapshots with a lifecycle rule
Snapshots accumulate; $0.015/GB-month is cheap but not free. R2 has prefix-scoped object lifecycle rules — bucket → Settings → Object Lifecycle Rules — that can delete objects after a set number of days or transition them to Infrequent Access. A sensible default: one rule on the backupdrill/prefix (the CLI’s default key prefix) expiring objects after your retention window. Skip the Infrequent Access transition for snapshots you still drill against: IA charges $0.01/GB to read data back and carries a 30-day minimum storage duration, so a drilled snapshot in IA pays a retrieval fee every drill — the FAQ below has the full trade-off.
Prove it restores
A snapshot in R2 is a file, not a recovery plan — the only way to know it restores is to restore it. Run backupdrill drill to pull the latest snapshot into a throwaway Docker Postgres and verify it against the manifest, or walk through the full restore-testing guide — on R2, reading the whole snapshot back for a drill costs exactly nothing in egress.
FAQ
Does Cloudflare R2 really have zero egress fees?
Yes. R2's pricing page lists egress bandwidth as free on both storage classes — you pay for storage ($0.015/GB-month on Standard) and per-operation fees ($4.50 per million writes, $0.36 per million reads on Standard), not for bandwidth out. One nuance for backups: the Infrequent Access class charges a $0.01/GB data-retrieval processing fee when you read objects back. That is not a bandwidth fee, but it means restores from Infrequent Access are not free the way restores from Standard are.
What endpoint and region does R2 use with S3 tools?
The S3 API endpoint is https://<account-id>.r2.cloudflarestorage.com — your account ID is shown on the R2 overview page and on the API token success page. Use auto as the region, and path-style addressing (the backupdrill CLI turns path-style on by default whenever an endpoint is set). Buckets created under the EU jurisdiction use a jurisdiction-specific endpoint instead: https://<account-id>.eu.r2.cloudflarestorage.com.
Should Supabase backups use R2's Infrequent Access storage class?
Usually not for the snapshots you restore-test. Infrequent Access costs $0.01/GB-month instead of Standard's $0.015, but adds a $0.01/GB retrieval fee, a 30-day minimum storage duration, and gets no free tier — and a restore drill reads the whole snapshot back, so drilled backups pay that retrieval fee on every drill. Keep recent snapshots in Standard; if you retain months of history, a prefix-scoped lifecycle rule can transition old snapshots to Infrequent Access or delete them outright.
Sources
- Cloudflare R2 docs — Pricing
- Cloudflare R2 docs — S3 API compatibility
- Cloudflare R2 docs — Data location
- Cloudflare R2 docs — Object lifecycles
- Supabase docs — Connecting to your database
- Supabase docs — Manage Egress usage
- Supabase — Pricing
Facts and prices last verified July 11, 2026 against the sources above. Written by the team behind BackupDrill.
Prefer the schedule, the restore drills, and the reports without owning the workflow? BackupDrill runs this same engine against your R2 bucket on a schedule — open the console (the Free plan covers weekly backups for one project), see the bucket setup docs for the shorter click-path version, or keep it DIY with the open-source CLI.