Skip to content

Supabase multiple environments: dev, staging, prod workflow

Published Jul 5, 2026 · 8 min read

The setup that works for most teams is: local development with the Supabase CLI, plus one separate Supabase project per deployed environment — typically staging and production. Schema changes only ever travel forward as versioned migration files, applied by CI with supabase db push, never by clicking around in the production dashboard. Everything else in this post — config drift, seeding, re-baselining, access control — is about keeping that simple rule true under real-world pressure.

Supabase itself recommends exactly this local → staging → prod flow in its managing environments guide. What the official docs don't cover is what happens six months in: auth settings that drifted apart, a hotfix applied straight to prod, a staging project nobody trusts anymore. That's the part this post is honest about.

Three tiers, three different mechanisms:

  • Dev: every developer runs the full Supabase stack locally via the CLI (supabase start, Docker required). Free, disposable, resettable in seconds.
  • Staging: a dedicated Supabase project that mirrors production's schema. Deployed to automatically when you merge to develop.
  • Production: its own project, deployed to only when you merge to main, ideally behind a CI approval gate.

One project per environment beats sharing a single project with prefixed tables or separate schemas — RLS policies, auth settings, and extensions are project-global, so "environments" inside one project always leak into each other. The main alternative is Supabase's branching feature (Pro plan), which spins up a preview database per pull request. Branching is great for short-lived PR previews; separate projects are better for a long-lived staging environment your whole team and external services point at. We compare the two in depth in branching vs separate projects, and walk through the staging project setup itself in how to set up a Supabase staging environment.

How do you develop locally with the Supabase CLI?

Initialize once, then treat the supabase/ directory as the source of truth for your schema:

supabase init          # creates supabase/config.toml
supabase start         # local stack in Docker (Studio, Postgres, Auth, ...)
supabase migration new create_posts

Write SQL into the generated file in supabase/migrations/, then:

supabase db reset      # rebuilds local DB from ALL migrations + seed.sql

If you prefer making changes in the local Studio UI, capture them afterwards instead of writing SQL by hand:

supabase db diff -f create_posts   # diffs local DB against migrations, writes a file

Both paths end the same way: a timestamped .sql file committed to Git. supabase db reset is the habit that keeps you honest — if your app doesn't survive a reset, your migrations are incomplete and staging will break the same way.

For an existing hosted project that was built through the dashboard, start by pulling its current schema down as a baseline migration:

supabase link --project-ref <project-id>
supabase db pull       # writes the remote schema as a migration file

How do you deploy migrations to staging and prod with GitHub Actions?

The pattern from Supabase's own guide uses three workflows: ci.yaml validates migrations on every PR, staging.yaml deploys on merge to develop, production.yaml deploys on merge to main. The deploy job is short:

name: Deploy to staging
on:
  push:
    branches: [develop]
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
    steps:
      - uses: actions/checkout@v4
      - uses: supabase/setup-cli@v1
        with:
          version: latest
      - run: supabase link --project-ref ${{ secrets.STAGING_PROJECT_ID }}
      - run: supabase db push

The production workflow is identical with PRODUCTION_PROJECT_ID and PRODUCTION_DB_PASSWORD. Three rules that save teams pain:

  1. Store per-environment secrets in GitHub, not in the repo. SUPABASE_ACCESS_TOKEN plus a project ID and DB password per environment.
  2. Add a manual approval on the production job (GitHub Environments with required reviewers). A migration that dropped a column on staging should require a human click before it drops it on prod.
  3. Run supabase migration list when things look off — it shows which migration versions are applied locally vs remotely, and is the fastest way to spot an environment that's behind.

How do you handle config drift between Supabase environments?

Migrations only cover the database. Everything else — auth providers, redirect URLs, email templates, edge function secrets — is per-project configuration, and it drifts silently. Typical failure: GitHub OAuth works on staging, fails on prod, because someone added the prod callback URL to the OAuth app but never updated site_url, or vice versa.

What helps:

  • Keep config.toml in Git as the reference. Auth providers live under [auth.external.*], with secrets loaded from .env via env() so nothing sensitive is committed.
  • Use [remotes.<env>] overrides for values that legitimately differ per environment. Supabase's branching configuration docs show the pattern — e.g. a staging block pointing at the staging project ref with its own seed paths.
  • Maintain a per-environment checklist for the things that can't be codified: OAuth app callback URLs, SMTP settings, custom domains, edge function secrets. Review it whenever you touch auth.
  • Never reuse secrets across environments. Staging should have its own OAuth app, its own SMTP credentials, its own service role usage. A leaked staging key must not open prod.

This is the least automated part of the whole workflow, and pretending otherwise would be dishonest. Drift here is a checklist problem more than a tooling problem — for now.

How should you seed dev and staging data?

Local dev is solved: put deterministic inserts in supabase/seed.sql and they run on every supabase db reset. Keep seeds small and boring — a couple of users, one org, enough rows to exercise every RLS policy.

Staging is harder. You have three options, roughly in order of preference:

  1. Synthetic seeds, same file as local. Predictable, safe, no PII. Downside: staging never sees production-shaped data volumes or edge cases.
  2. A scripted subset of prod data, anonymized. Best fidelity, but you own the anonymization script and its failure modes — one missed column is a GDPR incident.
  3. Full prod copy. Don't. Real user emails in an environment with weaker access control is how test notifications get sent to customers.

SupaClone's anonymized data clones are coming soon and target exactly option 2; today it clones structure only, which pairs with option 1.

When should you re-baseline staging from production?

Re-baseline — throw staging away and rebuild it from prod's actual state — when you no longer trust that staging matches production. The two classic triggers:

  • A hotfix was applied directly to prod (an index at 2 a.m., a patched function via the dashboard) and never backported as a migration. Prod and your migration history have diverged.
  • Staging accumulated experiments — half-tested columns, abandoned tables, tweaked policies — and "it passed staging" stopped meaning anything.

The manual route is pg_dump --schema-only from prod into a fresh project, but Supabase-managed schemas (auth, storage, realtime) make naive dumps fail or, worse, half-succeed — we cover the specific errors in the pg_dump/pg_restore guide. RLS policies are the sneakiest casualty: a restore can bring tables without their policies and you won't notice until data leaks in a test.

This re-baseline is the job SupaClone was built for. You connect source (prod) and a fresh, empty target project via Supabase OAuth — no connection strings — and it clones the structure: schemas, tables, RLS policies, functions, triggers, indexes, views, enums, extensions, with optional Storage buckets, Edge Functions, and Auth config. Under the hood it's native pg_dump/pg_restore with a baseline-aware plan that skips the Supabase-managed schemas correctly. Every run ends with a field-by-field verification report — cloned, skipped, failed, and manual steps (secrets are never copied; they're listed as manual steps instead). The source is only ever read, and the target must be empty, so re-baselining means: create a new staging project, clone into it, run the manual steps from the report, repoint your staging deploy secrets, delete the old project. The 14-day trial includes one successful clone, which is exactly one re-baseline.

Rebuild approachManaged schemas (auth/storage)RLS policiesVerificationData
Replay migration historyn/a (never touched)Only if in migrationsmigration list onlySeed scripts
Manual pg_dump/pg_restoreFrequent errors, manual excludesEasy to lose silentlyNone — you diff by handPossible, incl. PII risk
Supabase branchingHandledCopied to previewNoneSchema only, ephemeral
SupaCloneSkipped correctly by designCloned + verifiedField-by-field reportStructure only (data clones coming soon)

Honest caveat: if your migration history is clean and complete, replaying it into a fresh project via CI is free and sufficient — the docs describe exactly that. Re-baselining tools earn their keep when history and reality have diverged.

How should teams split access across environments?

  • Production: dashboard access for as few people as possible. Nobody needs the prod database password day-to-day — CI holds it as a secret. Supabase's own guidance: don't share the postgres password with the team.
  • Staging: broader dashboard access is fine — that's the point of staging — but schema changes should still arrive via CI, or staging drifts and you're back in the re-baseline section.
  • Local: everyone has full power over their own stack; no shared credentials involved at all.

If your Supabase org supports roles, give most developers read-ish access to prod for debugging and reserve owner/admin for the people who run deployments. And route every "quick prod fix" through a migration PR — the entire workflow stands or falls on that discipline.

FAQ

Do I need separate Supabase projects for staging and production?

Yes, for any team beyond a solo prototype. RLS, auth config, and extensions are project-wide, so environments sharing one project inevitably interfere. Separate projects give you real isolation and per-environment secrets; branching covers ephemeral PR previews on top.

Is Supabase branching enough instead of a staging project?

For per-PR schema previews, often yes. For a persistent environment that QA, stakeholders, and third-party webhooks point at, a dedicated staging project is more predictable. See branching vs separate projects for the full trade-off.

How do I copy my production schema into a new staging project?

Either replay your migration history with supabase link + supabase db push, or clone the live structure. Replaying works when history is complete; cloning (via careful pg_dump --schema-only or a structure clone) works when prod has drifted from the files.

Does SupaClone copy my production data to staging?

No. Today SupaClone clones structure only — schemas, tables, RLS policies, functions, triggers, indexes, views, enums, extensions, plus optional Storage, Edge Functions, and Auth config. Secrets are never copied. Exact and anonymized data clones are coming soon.

How do I stop auth settings drifting between environments?

Keep config.toml in Git with [remotes.<env>] overrides for legitimate differences, keep secrets in .env/CI, and maintain a short checklist for dashboard-only settings like OAuth callback URLs and SMTP. Review it on every auth-related change.

Clone your Supabase project without the pg_dump pitfalls

SupaClone copies schemas, tables, RLS policies, functions, and triggers into a fresh project — verified after every run. 14-day free trial, 1 clone included.

Start free trial