PostgreSQL

Supabase db push: must be owner of table (SQLSTATE 42501)

Why a migration that runs fine locally is refused on the remote project, which owner fixes work from the postgres role today, and when only Supabase support can help.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

supabase db push connects as the postgres role, and on a Supabase project that role is not a superuser. A migration that alters, drops or adds a constraint to a table owned by another role fails with must be owner of table <name> (SQLSTATE 42501), the whole migration file rolls back, and nothing is recorded. Find the table's owner. If you can act as that role, move the table to postgres and push again. If supabase_admin owns it, ask Supabase support. Granting privileges on the table does not help.

The error, as asked on Stack Overflow and reproduced on Supabase CLI 2.120.0:

text
Applying migration 20260113090000_add_display_name.sql...
ERROR: must be owner of table users (SQLSTATE 42501)
At statement: 0
alter table public.users add column display_name text
Try rerunning the command with --debug to troubleshoot the error.

Why the remote project refuses a migration your local stack accepts#

The PostgreSQL ALTER TABLE reference is blunt: "You must own the table to use ALTER TABLE." The privileges chapter adds that this right "can be inherited by members of the owning role", and a superuser bypasses the check. On a hosted project the CLI and the dashboard both connect as postgres, which is not a superuser. Supabase's troubleshooting page on realtime.messages states that "The postgres role is not a member of it and is not a superuser", and that page also says the SQL editor and supabase db push "Both connect as postgres."

The 2022 question hit this because of where the table came from. In supabase/cli issue #128, a Supabase engineer explained that "Right now tables created using the table editor is owned by supabase_admin". Supabase then removed superuser access from the SQL editor and, according to the October 2022 security patch notice, reassigned the entities created through the SQL Editor to the postgres role. In 2023 the same issue was closed with the note that "all your tables and schemas are owned by postgres role" and "If that is not the case, please file a support ticket". Today's roles documentation states that "Queries you run in the Dashboard execute as postgres".

So in 2026 the error usually means one of three things:

  • an old table kept a non-postgres owner;
  • the table was created by another login role, such as an ORM user;
  • the migration touches a schema that Supabase's internal roles own, such as auth or realtime.

What fails, and what does not happen#

The failure is all-or-nothing per file. The same troubleshooting page states that "supabase db push runs each migration file in one transaction, so the file rolls back and nothing is written to supabase_migrations.schema_migrations." The reproduction below confirmed this. The migration's second statement (create table public.profiles …) did not run, and the history table stayed empty:

text
 version | name 
---------+------
(0 rows)

You do not need supabase migration repair after this error. Fix the owner and run the same push again.

How this was reproduced#

The PostgreSQL part was reproduced locally on 11 October 2026 with Supabase CLI 2.120.0 (npm) against the official postgres:17 Docker image (PostgreSQL 17.11). The stand-in mimics a project's roles: postgres can log in but is not a superuser, and a superuser named supabase_admin owns public.users. Locally, supabase_admin is a superuser only because the image needs one. Supabase's documentation does not say whether that role is a superuser on hosted projects. The hosted behaviour (who the dashboard connects as, what support can change) comes from the Supabase documentation quoted above, not from this test. The local database has no TLS, so the test URL ends in ?sslmode=disable. A hosted connection string keeps TLS.

The migration used:

text
alter table public.users add column display_name text;
create table public.profiles (id bigint primary key references public.users (id), bio text);

Fix: find the owner, then move the table to postgres#

  1. Find who owns the table named in the error. Run this in the SQL editor or with psql on the project, replacing users with your table:

    sql
    select tableowner from pg_tables where schemaname = 'public' and tablename = 'users';

    In the reproduction, it returned supabase_admin:

    text
       tableowner   
    ----------------
     supabase_admin
    (1 row)

    To list every table in public that postgres does not own, use select tablename, tableowner from pg_tables where schemaname = 'public' and tableowner <> 'postgres'; instead.

  2. If the owner is a login role you created, for example a Prisma or Drizzle user, have postgres join that role and take over the table. The stand-in used a role called app_owner and a table called public.orders; substitute your own names. postgres cannot run ALTER TABLE ... OWNER TO on its own. The other role cannot hand the table over either, because the PostgreSQL reference requires that "To alter the owner, you must be able to SET ROLE to the new owning role". In the stand-in, app_owner was refused with must be able to SET ROLE "postgres". What worked, run as postgres:

    sql
    grant app_owner to postgres;
    alter table public.orders owner to postgres;
    text
    $ psql -U postgres -c "grant app_owner to postgres;"
    GRANT ROLE
     
    $ psql -U postgres -c "alter table public.orders owner to postgres;"
    ALTER TABLE

    In that test, postgres had created app_owner, on PostgreSQL 17. If the grant is refused on your project, connect as a role that is allowed to grant it.

  3. If the owner is supabase_admin, you cannot change it from the postgres role. The reproduction shows it:

    text
    $ psql -U postgres -c "alter table public.users owner to postgres;"
    ERROR:  must be owner of table users

    This is the case the CLI maintainers send to support ("please file a support ticket so that we can fix it for your project"). In the stand-in, the superuser ran the same statement, which gave the table to postgres, and the push went through.

  4. Push again. With a linked project (supabase link --project-ref your-project-ref replaces the 2022 db remote set, see below), preview the migrations first and then apply them:

    bash
    npx supabase db push --linked --dry-run
    npx supabase db push --linked

    Both flags are listed in the db push reference. For a database you are not linked to, pass --db-url with a percent-encoded connection string instead of --linked. Against a self-hosted Postgres without TLS, CLI 2.120.0 fails the TLS handshake and prints the message below; add ?sslmode=disable to that URL only, never to a hosted Supabase one:

    text
    This server does not accept TLS. Set `sslmode=disable` on the connection string to connect in plaintext.

Check your connection string#

If you push with --db-url, paste the string here to check the port, host and password encoding before you run the migration. It runs in your browser, and nothing is sent.

Runs entirely in your browser — nothing you paste is sent anywhere, and the password is masked in every result. Still cautious? Replace the password with any string containing the same special characters: the diagnosis only needs its shape.

Verify the fix#

After the owner change, the same migration file applied cleanly:

text
Applying migration 20260113090000_add_display_name.sql...
Finished supabase db push.

supabase migration list then shows the version on both sides, and a second push has nothing left to do:

text
   Local            | Remote           | Time (UTC)            
  ------------------|------------------|-----------------------
   `20260113090000` | `20260113090000` | `2026-01-13 09:00:00` 
text
Remote database is up to date.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

GRANT ALL is not ownership#

The first thing many people try is granting the postgres role every privilege on the table. In the reproduction, grant all privileges on table public.users to postgres; returned GRANT, and the next push failed with exactly the same must be owner of table users (SQLSTATE 42501). The PostgreSQL privileges chapter explains why: "The right to modify or destroy an object is inherent in being the object's owner, and cannot be granted or revoked in itself." ALTER TABLE, DROP TABLE and adding a foreign key constraint to the table (the case in issue #128) all need ownership.

Tables in auth and realtime: change the migration, not the owner#

Some tables are meant to stay with Supabase's own roles, and taking them over is not the fix.

  • realtime.messages is owned by supabase_realtime_admin. Supabase's troubleshooting page says that postgres may create, alter and drop policies on it, but that any alter table is refused. Its fix is to "Remove the ALTER TABLE line. Keep the policy". The usual offender is alter table realtime.messages enable row level security;, because RLS is already on.
  • The auth schema. The Auth 500-errors troubleshooting page expects objects there to be owned by supabase_auth_admin. It treats a "must be owner of..." error as a sign that ORM migrations or manual changes damaged that ownership, and it points to Support when supabase_admin owns those objects. The safer design is to keep your migrations on your own public tables, for example a profile table with a foreign key to auth.users, rather than altering auth.users itself.

When db push fails for a different reason#

Three other refusals show up while you clean up this error, and all three were reproduced with the same CLI.

You already ran the statement by hand. If you applied the change in the SQL editor while the push was failing, the next push fails on the objects that already exist:

text
ERROR: relation "profiles_bio_idx" already exists (SQLSTATE 42P07)

Record the version as applied without running it, using migration repair, and push again:

bash
npx supabase migration repair 20260113100000 --status applied --linked
npx supabase db push --linked
text
Repaired migration history: [20260113100000] => applied
Finished supabase migration repair.

A local file is older than the newest remote migration. This happens when you rename or create files out of order while fixing things:

text
Found local migration files to be inserted before the last migration on remote database.
 
Rerun the command with --include-all flag to apply these migrations:
supabase/migrations/20260113085000_profiles_website.sql

Running the push again with --include-all applied it. Do that only if the older file is still correct to run after the newer ones. The migration-workflow section of the zero-downtime Supabase migrations guide covers --dry-run before production, migration list --linked to inspect history, and using repair deliberately rather than deleting files.

The 2022 setup command no longer exists. The question configured the CLI with supabase db remote set. On CLI 2.120.0 that subcommand is gone:

text
Unknown subcommand "set" for "supabase db remote"

Link the project instead with supabase link, or pass --db-url on each command.

Hand-made changes in the dashboard are what usually create these ownership and history gaps. The Next.js + Supabase production mistakes article argues for putting every schema change in a migration file. The same owner rule explains a dashboard-side cousin of this error, must be the owner of event trigger pgsodium_trg_mask_update. If your migrations come from Prisma rather than SQL files, the failure to look at first is the pooled port, described in Can't migrate schema using Prisma with Supabase. For supabase db push, check who owns the table before you rewrite the migration: when the owner is right, the same file applies unchanged.

Further reading: Fix: Supabase local logs "Backend error" (and old 502s).

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote