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 --linkedreports success, but target branch policies remain unchanged- Branch reset recreates tables but uses the old (cached) migration content
migrations list --linkedshows migrations applied on a database that isn’t the intended target- Supabase Advisor warnings persist despite migration being tracked as applied
Investigation
Steps Tried
-
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.
-
supabase db push --linked(twice) — Both pushes went to production, not the staging branch.supabase linkconnects to the project’s production database, and--linkedalways targets that connection. The staging branch is a separate database only reachable via the GitHub integration. -
Incremental migration (DROP + CREATE policies) — Created
20260216000000_fix_advisor_warnings.sqlwithDROP POLICY IF EXISTS+CREATE POLICYfor 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 TRIGGERSET 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):
| Target | Method | Trigger |
|---|---|---|
| Staging | supabase db push --db-url | Push to main |
| Production | supabase db push --db-url | Push to production |
| Emergency fix | supabase 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:
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, andIF NOT EXISTS
Prevention
Best Practices
- Never run
supabase db push --linkedwithout verifying the target. Runsupabase db push --linked --dry-runfirst, or checkmigrations list --linkedand 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 --linkedsucceeding 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 listshowing 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:
- Which project is linked? (
supabase projects list) - Which database will this target? (
--linked= production,--db-url= explicit target) - Is this supposed to go through CI/CD instead? (Almost always yes)
- Run
--dry-runfirst 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
Supabase Advisor Warning Resolution Patterns Next
Tauri 2 Structured Error Serialization Produces [object Object]
Was this page helpful?
Thanks for your feedback!