Migrations workflow: local to SupaHost

Write and test SQL migrations locally with the Supabase CLI, then apply them to a SupaHost project with MCP apply_migration, Studio's SQL editor or psql over the SSH tunnel on Business.

A migration is a SQL file that moves your schema from one version to the next. Keeping migrations in git, testing them locally and applying the same files to production is the safest way to change a database that real users depend on.

With Supabase Cloud you would finish with supabase db push. That command needs a linked project, and linking uses Supabase's Management API, which SupaHost doesn't offer. Everything up to that step is the same; only the last step changes.

The loop

  1. Write the migration locally.
  2. Test it locally with a full reset.
  3. Commit it.
  4. Try it on a branch (optional, Pro and above).
  5. Apply it to the SupaHost project.

1. Write the migration

Either write SQL by hand:

npx supabase migration new add_booking_status
-- supabase/migrations/20261111140000_add_booking_status.sql
alter table public.bookings
  add column status text not null default 'pending'
  check (status in ('pending', 'confirmed', 'cancelled'));

create index bookings_status_idx on public.bookings (status);

Or make the change in local Studio (http://127.0.0.1:54323) and let the CLI write the file by comparing your local database with your migrations:

npx supabase db diff -f add_booking_status

Always read generated SQL before you commit it. Diff tools can turn a column rename into a drop and an add.

2. Test locally

npx supabase db reset

This rebuilds the local database from scratch: every migration in order, then the seed file. If it succeeds, your migrations work from an empty database. Run your app and your database tests against it.

3. Try it on a branch

On Pro and Business you can create a branch of your production project, with or without its data, from the project's Branches tab. A branch is a full project with its own URL, so you can apply the migration there first, point a preview deployment at it and check the result. Branches have no merge: once you're happy, apply the same migration to the parent project, then delete the branch.

4. Apply it to SupaHost

There are three ways. Pick one per project and stick to it, so you always know what has been applied.

With an AI agent over MCP (any plan)

If you've connected Claude Code, Cursor or another client to the SupaHost MCP server, ask it to apply the file:

Apply supabase/migrations/20261111140000_add_booking_status.sql to app-one with apply_migration, named add_booking_status.

The apply_migration tool runs the SQL and records it in supabase_migrations.schema_migrations in one transaction, so a failure leaves nothing half-done. The recorded version is the UTC time the migration was applied, not the timestamp in your file name. Use list_migrations to see what has run. Use a token without read-only access for this, and consider limiting it to one project.

In Studio's SQL editor (any plan)

Open the project, click Studio, go to the SQL editor, paste the file and run it. To make the change all-or-nothing, wrap it in a transaction:

begin;

alter table public.bookings
  add column status text not null default 'pending'
  check (status in ('pending', 'confirmed', 'cancelled'));

create index bookings_status_idx on public.bookings (status);

commit;

The SQL editor doesn't write to the migration history table. If you mix this with MCP, keep a note of which files you applied by hand.

With psql over the SSH tunnel (Business and Enterprise)

Business instances allow a restricted SSH tunnel to Postgres. Add your public key under Connect → SSH & CLI, then open the tunnel and copy the connection string from Show connection string:

ssh -N -L 5432:127.0.0.1:5432 [email protected]

In another terminal:

export DATABASE_URL='postgresql://postgres:<password>@127.0.0.1:5432/p_app_one'

psql "$DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction \
  -f supabase/migrations/20261111140000_add_booking_status.sql

--single-transaction and ON_ERROR_STOP mean the file either applies completely or not at all. If something on your machine already uses port 5432, forward another local port (for example -L 6543:127.0.0.1:5432) and change the port in the URL.

Because the CLI's database commands can take a Postgres URL instead of a linked project, you can also point them at the tunnel. On CLI version 3:

npx supabase migration list --remote="$DATABASE_URL"
npx supabase db push --remote="$DATABASE_URL"

On version 2 the flag is --db-url "$DATABASE_URL". These use the same supabase_migrations.schema_migrations table as MCP, so list_migrations shows CLI-applied migrations too. psql remains the simplest route if anything about the CLI's behaviour surprises you.

Starting from an existing SupaHost project

If the project already has tables that aren't in migration files, capture them as a baseline first:

  • Business: with the tunnel open, run npx supabase db dump --remote="$DATABASE_URL" -f supabase/migrations/20261101000000_baseline.sql (version 2: --db-url).
  • Other plans: on the Backups tab, click Create pg_dump, download the file, and extract the public schema as SQL:
pg_restore --schema-only --schema=public --no-owner --no-privileges \
  -f supabase/migrations/20261101000000_baseline.sql app-one-20261101-0900.dump

Review the baseline, run npx supabase db reset to check it builds a clean local copy, and don't apply it to production, which already has those objects.

Habits that prevent trouble

  • Never edit a migration that has been applied. Write a new one.
  • Make destructive changes in steps. Add the new column, backfill, switch the app over, and only drop the old column in a later migration.
  • Watch for long locks. Adding an index on a big table locks writes; use create index concurrently in its own migration, outside a transaction. See ALTER TABLE takes forever on a large table.
  • Back up first. On the Backups tab, Back up now takes a fresh backup before a risky change, and point-in-time recovery on Pro and Business lets you roll one project back.
  • Reload the API schema if needed. PostgREST normally notices schema changes; if a new column doesn't show up, run notify pgrst, 'reload schema';.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us