Skip to content

Using pgmt with Supabase

Supabase injects 25+ schemas into your database (auth, storage, realtime, vault, extensions, and more). pgmt works alongside Supabase — you manage your application schema, Supabase manages its platform infrastructure.

pgmt needs to know which schemas are yours and which belong to Supabase. You tell it with objects.include.schemas:

  • Your schemas (public, plus any you create) get managed by pgmt — cleaned, diffed, and migrated
  • Supabase schemas (auth, storage, realtime, etc.) are preserved in the shadow database and ignored during diffing
  • Foreign keys to Supabase tables (like auth.users) work because the shadow database runs the same Supabase PostgreSQL image
supabase init # If you haven't already
supabase start

This starts a local Supabase instance with PostgreSQL on port 54322.

pgmt init --dev-url postgres://postgres:postgres@127.0.0.1:54322/postgres --defaults

Update the generated config to use the Supabase PostgreSQL image and scope pgmt to your schemas:

databases:
dev_url: postgres://postgres:postgres@127.0.0.1:54322/postgres
shadow:
docker:
image: public.ecr.aws/supabase/postgres:17.6.1.081
environment:
POSTGRES_USER: supabase_admin
POSTGRES_PASSWORD: your-super-secret-and-long-postgres-password
objects:
include:
schemas:
- public

Why POSTGRES_USER: supabase_admin? The Supabase image’s init scripts expect a superuser named supabase_admin. If you omit this, pgmt defaults to POSTGRES_USER=postgres, and the Supabase init scripts fail — the container exits immediately with an unhelpful error. Always set POSTGRES_USER: supabase_admin when using the Supabase image.

Why the Supabase image? The shadow database needs the same extensions your schema might use (pgcrypto, pg_graphql, pgsodium, etc.). These are C extensions that only exist in the Supabase PostgreSQL build. If your schema only uses standard PostgreSQL features, you can skip the shadow.docker section entirely and use the default postgres:alpine image.

Why include.schemas? This tells pgmt to only manage the public schema. Without it, pgmt would try to diff and clean all 25+ Supabase schemas, generating incorrect migrations.

Add any additional application schemas you create:

objects:
include:
schemas:
- public
- app
- api

If your Supabase project already has tables, run pgmt init again now that pgmt.yaml is configured, and choose Update. Your config is used as the prompt defaults, and the import runs under it — include.schemas scopes both what gets imported and what the shadow database reset touches, so the Supabase platform schemas (auth, storage, …) are preserved and foreign keys to auth.users resolve during validation.

Create schema files as normal. You can reference Supabase objects like auth.users:

-- schema/profiles.sql
CREATE TABLE public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
display_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);

The foreign key to auth.users validates correctly because the shadow database runs the Supabase image, which includes the auth schema.

The standard pgmt workflow works unchanged:

# Apply schema to your local Supabase database
pgmt apply
# Preview what would change
pgmt diff
# Generate a migration for deployment
pgmt migrate new "add profiles table"

Migrations will only contain changes to your managed schemas — no Supabase platform objects.

Generated migrations can be applied to your hosted Supabase project:

pgmt migrate apply --target-url "$SUPABASE_DB_URL"

Or use the Supabase dashboard to run the migration SQL manually.

pgmt generates migration filenames without a prefix by default (e.g., 1734567890_add_profiles_table.sql), which is compatible with Supabase’s migration format. To use pgmt-generated migrations with supabase db push:

  1. Configure pgmt to write migrations into Supabase’s migrations directory:
directories:
migrations_dir: supabase/migrations
  1. Generate migrations normally:
supabase/migrations/1734567890_add_profiles_table.sql
pgmt migrate new "add profiles table"
  1. Deploy with supabase db push:
supabase db push

Trade-offs: Multi-section migrations (concurrent index creation, retry logic) won’t work through supabase db push — use pgmt migrate apply for those. For simple DDL migrations, either deployment method works.

Local development: supabase db reset works as a “clean slate” option, while pgmt apply --watch is recommended for iterative development since it applies changes incrementally without resetting your data.