Supabase Preview Branches¶
Each git feature branch can get its own isolated Supabase Postgres branch — independent DB, independent URL + API keys, automatically destroyed when the matching PR closes.
This page is the developer reference for the ./bootstrap.py --branch flow, the lifecycle workflows, and troubleshooting.
When to use a preview branch¶
| Use case | Recommended target |
|---|---|
| Editing migrations / RLS / RPCs | Preview branch (you need a real DB to test against) |
| Building UI against schema-stable data | Local Supabase (cd supabase && just dev) is faster |
| Reproducing a real client bug | Either, with just seed-branch-copy / just seed-local-copy to pull prod data |
| Pure-frontend changes | None — your branch can talk to staging |
The preview helper rebuilds newly created branches from the checkout's
supabase/migrations/** after Supabase's initial replay of production's recorded
migration SQL. Production changes missing from the checkout are not retained.
Closing the PR, merged or not, deletes the preview; it never writes production (see lifecycle).
Previews are not a copy of production data — they start empty and rely on
just seed-branch (synthetic) or just seed-branch-copy (real-prod copy) to populate.
Create a preview branch¶
From any non-protected git ref (develop / main / master are refused):
This runs (in order):
- Downloads env files from Secret Manager (
just download-env all). - Calls Supabase Management API to ensure a branch exists for the current git ref (slug =
slugify(git_branch_name)). - Waits for provisioning and for Supabase's initial migration replay to settle, even if that replay failed.
- Preserves the branch's existing PostgREST configuration and exposes the schemas in
REQUIRED_EXPOSED_SCHEMAS(currentlybackoffice). - Writes credentials into:
backend/.env.local(SUPABASE_URL,SUPABASE_ANON_KEY,SUPABASE_SERVICE_ROLE_KEY,POSTGRES_*)frontend/.env.local+frontend/.env.development.local(VITE_SUPABASE_URL,VITE_DIRECT_SUPABASE_*, …)
- Rebuilds a fresh preview's application schemas and migration history from the checkout's migration files. Reused previews are also rebuilt if their applied migration count is behind the checkout or unreadable; otherwise their schema and seeded data are preserved.
- Runs
just seed-branch— applies any missing migrations, creates dev users, inserts synthetic data, refreshes materialized views.
After this, just dev <environment> from backend/ and cd frontend && just dev talk to your branch DB, not production Supabase.
Day-to-day commands¶
# Create / refresh a branch + seed it (idempotent — safe to re-run)
./bootstrap.py --branch
# Apply migrations and re-seed after changing the seed function
cd supabase && just seed-branch
# Wipe + re-seed synthetic data
cd supabase && just seed-branch --clear
# Pull real production data instead (slow, contains PII)
cd supabase && just seed-branch-copy
# Manage branches directly
python3.12 scripts/supabase_branch.py ensure
python3.12 scripts/supabase_branch.py delete <git-branch-name>
python3.12 scripts/supabase_branch.py reap # delete orphans (dry-run with --dry-run)
supabase db push against a preview branch is safe — branches are isolated. The just seed-branch target already does this for you, but you can also push manually:
cd supabase
set -a; . ../backend/.env.local; set +a
DB_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
supabase db push --db-url "$DB_URL" --include-all --yes
Lifecycle¶
| Event | Trigger | Action |
|---|---|---|
| PR closed (merged or not) | .github/workflows/supabase-branch-cleanup.yml |
Calls supabase_branch.py delete <head_ref> — just removes the preview; missing previews are a no-op. It never writes production: migrations reach the develop test database and production through the Supabase GitHub integration (see branch mapping). |
| Nightly 03:00 UTC | .github/workflows/supabase-branch-reaper.yml |
Calls supabase_branch.py reap — deletes branches whose upstream git ref is gone, idle ≥ 7d with no open PR, or older than 30d (hard cap). Never touches the default (production) branch or persistent branches such as the develop test database. |
scripts/supabase_branch.py internals¶
Token resolution¶
SUPABASE_ACCESS_TOKEN is looked up in this order, first match wins:
- Shell environment.
- Any
backend/.env.*file. - GCP Secret Manager (
gcloud secrets versions access latest --secret=SUPABASE_ACCESS_TOKEN).
The script does not read the Supabase CLI's own credential store. If you are logged
into the CLI but have no token in env or backend/.env.*, recover it without printing it
(macOS — -g would dump the token to your terminal):
export SUPABASE_ACCESS_TOKEN=$(security find-generic-password -s "Supabase CLI" \
-a access-token -w | sed 's/^go-keyring-base64://' | base64 -d)
When all three fail, the script exits with code 2 and a multi-line banner:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✗ SUPABASE_ACCESS_TOKEN missing — cannot reach Supabase Management API.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
…
Fix one of:
1. cd backend && just download-env all
2. export SUPABASE_ACCESS_TOKEN=<token>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Auto-recreate broken branches¶
The branch lifecycle uses these statuses (subset of what the Supabase API returns):
| Set | Statuses | Behavior |
|---|---|---|
| Healthy | ACTIVE_HEALTHY, ACTIVE, FUNCTIONS_DEPLOYED |
Continue to migration-history checks. |
| Initial replay failed | MIGRATIONS_FAILED |
Continue; a fresh or behind preview is rebuilt from Git. |
| Broken | FAILED, REMOVED |
Existing branch: delete and recreate once. |
| Transient | CREATING_PROJECT, COMING_UP, … |
Poll until terminal status. |
If provisioning or the readiness wait fails for an existing branch, the helper
deletes and recreates it once. MIGRATIONS_FAILED alone does not trigger deletion.
The subsequent Git replay can still fail; inspect that error before retrying.
Required exposed schemas¶
After a branch becomes healthy, ensure reads GET /v1/projects/{branch_ref}/postgrest and appends any missing REQUIRED_EXPOSED_SCHEMAS through the corresponding PATCH endpoint. Existing schemas remain in their current order, and re-running ensure is a no-op when the required schemas are already present.
Add a schema to this list only when browser code must address it through supabase.schema(...). Production exposure remains an explicit Dashboard setting.
Empty db_pass recovery¶
The Supabase Management API does not reliably return the branch DB password in the create payload (it sometimes comes back as an empty string). When that happens, the script:
- Generates a 24-byte URL-safe random password.
- Calls
PATCH /v1/projects/{branch_ref}/database/passwordwith that value. - Persists the new password into
backend/.env.localasPOSTGRES_PASSWORD.
This is logged as Resetting DB password for branch <ref> (API returned empty db_pass). Subsequent psql connections use the persisted password.
Common errors¶
❌ POSTGRES_PASSWORD is empty in backend/.env.local¶
The branch was created but the API returned empty db_pass and the password-reset path didn't fire (e.g. you ran an old version of supabase_branch.py). Run ./bootstrap.py --branch again — the latest version always resets when empty.
❌ Branch DB is not provisioned: public.clients does not exist¶
The branch is alive but migrations didn't replay. Two recovery paths:
# Option 1: let bootstrap recreate it
python3.12 scripts/supabase_branch.py delete $(git rev-parse --abbrev-ref HEAD)
./bootstrap.py --branch
# Option 2: push migrations manually
cd supabase
set -a; . ../backend/.env.local; set +a
DB_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
supabase db push --db-url "$DB_URL" --include-all --yes
Access Restricted: admin@admin.com does not have access¶
The browser holds a JWT signed by a previous (deleted) branch's secret. Clear site data for localhost:3002 (or whichever port the backoffice runs on) and log in again — the new JWT will be signed by the current branch's secret.
Invalid login credentials¶
seed.sql didn't run on the branch (auth users missing). Re-run cd supabase && just seed-branch — that runs seed.sql as part of the flow.
captcha protection: request disallowed (no captcha_token found)¶
Preview branches inherit production's Auth settings — CAPTCHA (Attack Protection) and
the password policy. None of it is database state, so no seed or migration can change it.
Both the login form and the dev auto-login hit /auth/v1/token?grant_type=password with
no captcha token, so every password sign-in on the branch is rejected.
Disable it on the branch project only (never production): Dashboard → branch project → Authentication → Attack Protection → CAPTCHA off. Headless equivalent:
curl -sS -X PATCH "https://api.supabase.com/v1/projects/<branch_ref>/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"security_captcha_enabled": false}' >/dev/null
Never GET that endpoint and print the response: it carries the inherited production
Turnstile secret key, which then lands in your shell history or an agent transcript.
The same inheritance is why just seed-branch generates a random
BACKOFFICE_ADMIN_PASSWORD instead of admin — prod's policy (min 8 + HIBP) rejects the
weak seed password on a branch.
Configuration¶
| Env var | Purpose |
|---|---|
SUPABASE_ACCESS_TOKEN |
Personal token from https://supabase.com/dashboard/account/tokens. Stored in rose-backend-env-* Secret Manager blobs. |
SUPABASE_PROJECT_REF |
Override the production project ref (defaults to Rose production). |
PROTECTED_BRANCHES |
Hard-coded in scripts/supabase_branch.py — {develop, main, master, HEAD}. |
CREATE_POLL_TIMEOUT_S |
300s. How long ensure waits for branch to become healthy. |
REQUIRED_EXPOSED_SCHEMAS |
Custom PostgREST schemas appended to every preview branch (backoffice). |
REAPER_STALE_DAYS |
7d idle threshold for reaper. |
REAPER_HARD_CAP_DAYS |
30d max age cap. |
See also¶
- Supabase Setup — Auth flow, RLS architecture,
before_user_createdhook. - Synthetic Seed — what
just seed-branchputs in the DB. supabase/AGENTS.md— essential agent safeguards and task links into these shared procedures.docs/plans/seed-synthetic-data.md— original design doc.docs/plans/seed-pgdump-study.md— performance study for the*-copy(real-data) path.