Skip to content
Documentation GitHub
Infrastructure

Supabase Branch Migration Deployment: CLI vs GitHub Integration

Supabase Branch Migration Deployment: CLI vs GitHub Integration

Problem

After editing a baseline migration in-place to fix Supabase Advisor warnings (RLS policy performance, trigger security, missing FK indexes), pushing to Git and resetting the staging branch did not apply the corrected migration. Subsequent supabase db push --linked commands silently targeted the production database instead of the staging branch.

Symptoms:

  • supabase db push --linked reports success, but target branch policies remain unchanged
  • Branch reset recreates tables but uses the old (cached) migration content
  • migrations list --linked shows migrations applied on a database that isn’t the intended target
  • Supabase Advisor warnings persist despite migration being tracked as applied

Investigation

Steps Tried

  1. Edit baseline in-place + push to Git + reset branch — Branch reset recreated tables but with old policy definitions. The branch caches migration content from the GitHub integration’s first application; editing the file and re-pushing doesn’t update the cached copy because the migration name is unchanged.

  2. supabase db push --linked (twice) — Both pushes went to production, not the staging branch. supabase link connects to the project’s production database, and --linked always targets that connection. The staging branch is a separate database only reachable via the GitHub integration.

  3. Incremental migration (DROP + CREATE policies) — Created 20260216000000_fix_advisor_warnings.sql with DROP POLICY IF EXISTS + CREATE POLICY for all 34 RLS policies. This worked correctly when applied via Git push + GitHub integration.

Root Cause

Two distinct issues compounded:

1. supabase db push --linked always targets production

When you run supabase link --project-ref <ref>, the CLI connects to the project’s production database. All subsequent db push --linked commands target that production database, regardless of which branch you’re viewing in the dashboard.

Staging/preview branches receive migrations exclusively through the GitHub integration, triggered by Git pushes to the linked Git branch.

2. Migrations are immutable by name

Like any migration framework, Supabase tracks migrations by their filename/timestamp. Once a migration has been applied (or cached by the GitHub integration), editing its contents and re-pushing has no effect — the system sees “migration X already applied” and skips it.

PostgreSQL compounds this for RLS policies specifically: there is no CREATE OR REPLACE POLICY syntax. Policies must be explicitly dropped and recreated, which requires a new incremental migration.

Solution

For the migration content fix

Create an incremental migration that is idempotent and can run on any branch:

-- Drop and recreate policies (no CREATE OR REPLACE POLICY in PostgreSQL)
DROP POLICY IF EXISTS "policy_name" ON table_name;
CREATE POLICY "policy_name" ON table_name
FOR SELECT USING (column = (select auth.uid()));
-- Functions support CREATE OR REPLACE (idempotent)
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER
SET search_path = ''
AS $$ ... $$ LANGUAGE plpgsql;
-- Indexes support IF NOT EXISTS (idempotent)
CREATE INDEX IF NOT EXISTS idx_name ON table_name (column);

For deploying to branches

Never use supabase db push --linked to target a branch. The --linked flag always targets production, not the branch you’re viewing in the dashboard.

All migration deployment is managed by the CI/CD pipeline (deploy.yml):

TargetMethodTrigger
Stagingsupabase db push --db-urlPush to main
Productionsupabase db push --db-urlPush to production
Emergency fixsupabase db push --db-url <connection-string>Manual

To get a branch’s connection string: Dashboard > Settings > Database (while viewing the target branch).

Disabling auto-migration via the GitHub integration

The Supabase GitHub integration automatically applies migrations to any persistent branch when commits land on its associated git branch. There is no toggle to disable this behavior without removing the integration entirely (which requires deleting all branches first).

Workaround: Re-associate the persistent branch with a non-existent git branch so the integration has nothing to trigger on:

Terminal window
supabase --experimental branches update <branch-id> \
--git-branch '_no-auto-deploy' \
--project-ref <project-ref>

This keeps the persistent branch alive and the GitHub integration connected, while the CI/CD pipeline becomes the sole path for migration application.

Deployment flow

Local edit → git push to main → CI quality gates → staging (db push --db-url)
→ git push to production → CI quality gates → production (db push --db-url)

Each GitHub Environment (staging, production) holds one secret: SUPABASE_DB_URL — the Postgres connection string for the target database.

Implementation Notes

  • The corrected baseline (00000000000000_baseline.sql) ensures fresh deployments get the fix automatically
  • The incremental migration (20260216000000_fix_advisor_warnings.sql) fixes branches where the old baseline already ran
  • Both migrations coexist safely: the incremental uses DROP IF EXISTS, CREATE OR REPLACE, and IF NOT EXISTS

Prevention

Best Practices

  • Never run supabase db push --linked without verifying the target. Run supabase db push --linked --dry-run first, or check migrations list --linked and confirm which database you’re connected to.
  • Treat migrations as immutable. If a migration needs correction after it has been applied anywhere, create an incremental migration — never edit the original in place as the sole fix.
  • Edit the baseline AND create an incremental. Fix the baseline for fresh deployments, add an incremental for existing branches. Both are needed.
  • PostgreSQL policy changes always need DROP + CREATE. Unlike functions (CREATE OR REPLACE) and indexes (IF NOT EXISTS), policies have no idempotent creation syntax.

Warning Signs

  • supabase db push --linked succeeding but dashboard showing no change — you’re pushing to a different database than you’re viewing
  • Branch reset creating tables but policies/functions unchanged — the branch replayed cached (old) migration content
  • migrations list showing migrations as applied on Remote but the schema doesn’t reflect changes — the migration SQL errored silently (objects already exist) but the tracking entry was still recorded

CLI Safety Checklist

Before any manual supabase db push:

  1. Which project is linked? (supabase projects list)
  2. Which database will this target? (--linked = production, --db-url = explicit target)
  3. Is this supposed to go through CI/CD instead? (Almost always yes)
  4. Run --dry-run first to verify which migrations would be applied

References

  • Commit d386db0 — baseline migration fix (in-place edit)
  • Commit 749d502 — incremental migration for existing branches
  • Supabase Advisor (splinter linter) — flagged the original 35 warnings + 6 suggestions

Was this page helpful?