# 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.

Source: https://supahost.app/docs/local-development/migrations-workflow · Updated 2026-11-11

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:

```bash
npx supabase migration new add_booking_status
```

```sql
-- 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:

```bash
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

```bash
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](/docs/local-development/testing) against it.

## 3. Try it on a branch

On Pro and Business you can create a [branch](/docs/platform/branching) 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](/docs/ai-tools/mcp), 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:

```sql
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**:

```bash
ssh -N -L 5432:127.0.0.1:5432 supahost@acme.supahost.space
```

In another terminal:

```bash
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:

```bash
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:

```bash
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](/kb/slow-alter-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';`.

## Related

- [SQL migrations](/docs/database/migrations)
- [MCP tools reference](/docs/ai-tools/mcp-tools)
- [SSH access and direct Postgres](/docs/platform/ssh)
- [Partial migrations from transaction mistakes](/kb/partial-migrations)
