Sign in

Published

Why supabase db diff Is Not a Substitute for Declarative sync

You edit a SQL definition in supabase/schemas/, run a migration-generation command, and get no changes. The SQL may be fine. The problem may simply be that the command never looked at your file.

In Supabase’s pg-delta declarative workflow, supabase db diff and supabase db schema declarative sync compare different inputs. Both can generate migration SQL, but they answer different questions. That distinction determines which changes they can—and cannot—see.

The important question: which two states are being compared?

A schema diff is meaningful only after you identify its starting state and desired state. In this workflow, there are three relevant representations of your schema:

  • Migration-derived state: the database schema produced by your existing migrations.
  • Declarative state: the definitions in your declarative SQL files, such as those under supabase/schemas/.
  • Live database state: the schema currently present in a running local or specified remote database.

The two commands select different pairs:

CommandStarting stateDesired stateReads declarative schema files under pg-delta?
supabase db schema declarative syncSchema produced by existing migrationsDeclarative SQL definitionsYes
supabase db diffShadow database built from local migrationsLive local database, or a specified remote databaseNo

The CLI describes db diff in terms of a shadow database: a database built from your local migrations and compared with the target database. Declarative sync, by contrast, generates a migration from the difference between your migration-derived state and your declarative definitions. See the declarative schema guide and CLI reference.

Edited definition files → sync. Edited a running database → db diff.

The fact that both commands can emit SQL does not make them interchangeable. Their output format is similar; their inputs are not.

A file edit that db diff cannot see

Suppose your existing migrations create this table:

create table public.employees (
  id bigint primary key,
  name text
);

Your local database has been built from those migrations, so it also contains only id and name.

Now you update supabase/schemas/employees.sql:

create table public.employees (
  id bigint primary key,
  name text,
  age smallint
);

At this point, your three states differ:

RepresentationColumns in public.employees
Existing migrationsid, name
Declarative fileid, name, age
Live local databaseid, name

What declarative sync sees

Run:

supabase db schema declarative sync -f add_age --no-apply

The comparison is between:

  • The migration-derived table, which has no age column.
  • The declarative definition, which includes age smallint.

That difference can produce migration SQL equivalent to:

alter table public.employees
  add column age smallint;

The live database does not need to contain the new column for this comparison to detect it. The file edit is the input.

What db diff sees

Run instead:

supabase db diff -f add_age

Under pg-delta, this compares:

  • The shadow database built from your migrations.
  • Your live local database.

Both still have only id and name. There is no new column to detect.

An empty diff here does not mean that your declarative files match your migration history. It means that the two database states selected by db diff have no relevant difference.

That is the practical danger of substituting commands: you can mistake a comparison of the wrong inputs for confirmation that there is nothing to migrate.

The reverse case: a Studio edit that sync cannot see

Now start again from the original table, but make a different change: add age smallint directly to the local database through Studio, without updating your declarative file.

Your states now look like this:

RepresentationColumns in public.employees
Existing migrationsid, name
Declarative fileid, name
Live local databaseid, name, age

This time:

  • db diff can detect the new column, because the live database differs from the migration-built shadow database.
  • Declarative sync does not detect that live edit, because the declarative file still agrees with the migration-derived state.

This is not a limitation you can fix by rerunning sync. The live database change is outside its comparison.

If declarative files are your source of truth, you need to express the intended change in those files. Do not assume that a Studio edit will later be discovered and incorporated automatically by declarative sync.

A live database edit and a declarative file edit are separate changes until you deliberately reconcile them.

Why older instructions can lead you astray

The warning matters specifically when moving to the pg-delta declarative workflow.

In the older migra declarative workflow, supabase db diff could read declarative files when they were present. Consequently, an older tutorial might prescribe:

  1. Edit a definition in supabase/schemas/.
  2. Run supabase db diff.
  3. Review the generated migration.

With pg-delta, the declarative-file comparison belongs to:

supabase db schema declarative sync

db diff retains its role of comparing a live database against the migration-built state.

So the command choice is not merely a matter of preferred syntax. Reusing the older sequence changes what you compare—and may silently omit the file change you intended to capture.

Choose the command from the source of the change

For a pg-delta declarative project, your normal file-driven workflow is:

  1. Edit the declarative SQL definition.
  2. Generate the migration with declarative sync.
  3. Review the generated SQL to confirm it expresses the intended change.

For example:

supabase db schema declarative sync -f add_age --no-apply

Use db diff when you deliberately want to capture or inspect changes in a live database relative to your migrations.

The reliable mental model is simple: migration generation follows the comparison inputs, not the command’s name or the fact that it produces SQL. Before treating an empty diff as evidence that your schema is up to date, make sure the command actually inspected the representation you changed.