Diff engines: pg-delta and migra
How the Supabase CLI generates migrations, which engine your project uses, and how to roll back.
A diff engine compares two database states and generates the SQL that turns one into the other. It powers supabase db diff, supabase db pull, and the supabase db schema declarative commands. The CLI ships two engines, and the same command can behave differently depending on which one your project uses. This page explains how to tell which engine you're on, what differs between them, what to expect when an existing project upgrades to pg-delta, and how to roll back to migra.
pg-delta is Supabase's open source engine and the default for every project. Rather than parsing SQL to understand your schema, it loads each state into a real Postgres instance and reads the result from the catalog. It models entities the legacy engine missed, such as comments, domains, roles, publication membership, and the security invoker setting on views, and it supports Postgres 14 through 18. It ships under the [experimental.pgdelta] config namespace and remains pre-1.0.
migra is djrobstep's open source Python library for diffing Postgres schemas, and the CLI has shelled out to it since before pg-delta existed. It compares two live Postgres databases and generates the SQL that turns one into the other, the mechanism behind supabase db diff. Projects that opt out of pg-delta continue to use it, and every command they relied on keeps working.
Which engine your project uses#
The CLI uses the pg-delta diff engine unless config.toml opts out. If it contains an [experimental.pgdelta] section with enabled = false, your project uses the legacy migra engine. If the section is missing, or enabled is true or omitted, your project uses pg-delta. See Diff engines for how the two differ and how to roll back.
pg-delta is the default, so a project uses it without any configuration. supabase init still writes the setting explicitly so the choice is visible in config.toml:
[experimental.pgdelta]enabled = trueExisting projects move to pg-delta when you upgrade the CLI, whether or not config.toml mentions the section. See Upgrade an existing project to pg-delta for what to expect on the first pull.
To use migra for a single run without changing config.toml, the flag depends on the command:
| Command | Flag to fall back to migra |
|---|---|
db diff | --use-migra |
db pull | --diff-engine migra |
To opt out entirely, set enabled = false under [experimental.pgdelta]. The SUPABASE_EXPERIMENTAL_PG_DELTA environment variable from earlier CLI versions is ignored, so the config setting is the only project-wide switch.
What differs between the engines#
The commands and the migration workflow are the same on both engines. What differs is what each command reads, what it captures, and what it writes.
| Behavior | pg-delta | Legacy migra |
|---|---|---|
What db diff compares | Always the live database against a shadow database built from your migrations. Never reads supabase/schemas/. | The live database against your migrations, unless supabase/schemas/ contains declarative files. Then it compares those files against your migrations instead. |
| Generating migrations from declarative files | supabase db schema declarative sync | supabase db diff, with the local stack stopped |
| Exporting a database into declarative files | supabase db schema declarative generate | Not available. Use supabase db dump and split the output by hand. |
| Ordering of declarative files | Automatic, based on dependencies between statements. [db.migrations].schema_paths is ignored, and the CLI warns when the setting lists any paths. | Lexicographic by default. [db.migrations].schema_paths overrides the order. Files in supabase/schemas/ are used even when the setting is empty. |
Initial db pull (empty migration history) | Diffs the remote database against an empty shadow database. Your customizations in managed schemas, such as triggers on auth.users and RLS policies on storage.objects, are included. | Seeds the migration with pg_dump, which skips managed schemas, then appends a diff of your triggers and RLS policies in auth and storage. Older versions excluded those schemas and told you to run supabase db pull --schema auth,storage. |
Later db pull runs | Diffs the remote database against your migrations. | Same. Trigger and RLS policy changes in managed schemas are included. |
db pull --declarative | Replaces supabase/schemas/ from the selected database without creating a migration. | Runs, but don't use it. The tree it writes is only readable by the db schema declarative commands. db diff on the legacy engine then tries to load that tree as its baseline and fails. |
| Objects the engine doesn't track | Reported as warnings and never silently dropped. Pass --strict-coverage to turn the warnings into failures. | Silently omitted. --strict-coverage has no effect. |
| Generated SQL | Uppercase keywords wrapped at 180 characters. Configurable with [experimental.pgdelta] format_options. | Engine default formatting. |
| Migration files per run | Usually one. When a change crosses a transaction boundary, it may write one ordered file per unit with _1, _2 suffixes. Files whose statements can't run in a transaction start with -- pg-delta: transaction=false. | One file. |
| Known limitations | See Known caveats. | See Limitations of the legacy engine. |
For a walkthrough of the day-to-day commands on pg-delta, see Local development workflow. For the declarative workflow on each engine, see Declarative database schemas.
Upgrade an existing project to pg-delta#
Existing projects move to pg-delta automatically when you upgrade to a CLI version where it is the default. No config.toml change is needed. What needs attention is the first db pull after the upgrade and any scripts that relied on migra behavior. Do the first pull on a branch so you can review everything before it reaches your team.
-
Upgrade the CLI. If you want the engine choice visible to your team, add the setting
supabase initwrites for new projects. The CLI behaves the same without it:supabase/config.toml[experimental.pgdelta]enabled = true -
If your project uses declarative schemas, remove
[db.migrations].schema_paths. Ordering is automatic onpg-delta, and the CLI warns when the setting lists any paths. -
Pull once against your linked project to create a catch-up migration:
supabase db pullIf your history was built from legacy diffs, it doesn't mention objects the legacy engine didn't track, such as comments, domains, roles, and publication membership. This first pull captures them in a single catch-up migration. Your remote database already has these objects, so accept the prompt to record the migration as applied and review the file like any other generated migration. If your baseline came from the legacy engine's
pg_dumppath, it already contains those objects, and this pull reports "No schema changes found". There is nothing to commit in that case. See Cleaning up generated migrations. -
Replace
supabase db diffwithsupabase db schema declarative syncwherever you generate migrations from declarative files, including shell scripts, CI jobs, and AI prompt files such asexamples/prompts/declarative-database-schema.md. You no longer need to runsupabase stopbefore generating. If the CLI didn't generate your schema tree, see Adopting an existing schema tree for two checks that run on the firstsync. -
Verify the full migration chain locally:
supabase db reset -
Optionally, add
supabase db diff --strict-coverageto CI so untracked objects fail the job instead of producing warnings. See Managing environments.
If you deploy through the GitHub integration, branching runs every migration inside a transaction and doesn't honor the -- pg-delta: transaction=false directive. A migration that carries it applies with supabase db push but fails when branching deploys it.
To roll back, set enabled = false under [experimental.pgdelta], or use the per-command flags above for a single run. Migrations already generated by pg-delta are plain SQL and keep working on either engine.