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:
| Command | Starting state | Desired state | Reads declarative schema files under pg-delta? |
|---|---|---|---|
supabase db schema declarative sync | Schema produced by existing migrations | Declarative SQL definitions | Yes |
supabase db diff | Shadow database built from local migrations | Live local database, or a specified remote database | No |
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:
| Representation | Columns in public.employees |
|---|---|
| Existing migrations | id, name |
| Declarative file | id, name, age |
| Live local database | id, 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
agecolumn. - 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:
| Representation | Columns in public.employees |
|---|---|
| Existing migrations | id, name |
| Declarative file | id, name |
| Live local database | id, name, age |
This time:
db diffcan detect the new column, because the live database differs from the migration-built shadow database.- Declarative
syncdoes 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:
- Edit a definition in
supabase/schemas/. - Run
supabase db diff. - 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:
- Edit the declarative SQL definition.
- Generate the migration with declarative
sync. - 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.