authSQLSTATE 42501

permission denied for schema public

What it means

"permission denied for schema public" (SQLSTATE 42501) means the current role lacks USAGE or CREATE on the schema it is touching. Since Postgres 15 the PUBLIC role no longer gets CREATE on schema public by default, so roles that used to work now fail on CREATE TABLE. Fix it by granting the missing privilege to the role, or by running the DDL as the schema owner or the postgres superuser.

Why it happens

The message is Postgres SQLSTATE 42501 (insufficient privilege). The role on the current connection is trying to read, create, or alter an object in a schema for which it lacks the required privilege. There are three common causes during a Supabase migration:

  1. Postgres 15 removed the implicit CREATE grant. Before Postgres 15, the built-in PUBLIC role had CREATE on schema public, so almost any role could run CREATE TABLE. Postgres 15 dropped that default. A role that migrated fine on PG 14 now fails the moment schema replay runs its first CREATE TABLE.

  2. Running DDL as anon or authenticated. These Supabase roles are meant for row-level API access, not schema changes. They typically have USAGE on public but not CREATE, and no rights at all on auth, storage, or other reserved schemas. If your connection string or pooler session resolves to one of these roles, DDL fails.

  3. The target object is owned by another role. ALTER TABLE, DROP, or CREATE INDEX on an existing object requires ownership (or superuser). If the destination already has objects owned by postgres and you connect as a different role, you get 42501 on the alter even though you can SELECT the table.

How to fix it

First, confirm which role is failing and what it currently has. Run this on the exact connection that errored:

select current_user, session_user;

-- privileges the current role has on schema public
select
  has_schema_privilege(current_user, 'public', 'USAGE')  as has_usage,
  has_schema_privilege(current_user, 'public', 'CREATE') as has_create;

\dn+ lists every schema with its owner and ACLs (the psql meta-command; use the query below if you are not in psql):

select n.nspname as schema,
       pg_catalog.pg_get_userbyid(n.nspowner) as owner,
       n.nspacl as privileges
from pg_catalog.pg_namespace n
where n.nspname = 'public';

If has_create is false, grant the missing privileges. Run these as postgres (or the schema owner):

grant usage  on schema public to authenticated;
grant create on schema public to authenticated;

Replace authenticated with whatever current_user returned. For a dedicated migration role:

grant usage, create on schema public to migrator;

If the failure is on an existing object owned by someone else, either reassign ownership or run the DDL as the owner. To transfer ownership so your role can alter it:

alter table public.orders owner to migrator;

The simplest fix for the whole migration is to connect as postgres. On a Supabase destination, the direct-connection URI (not the pooler's anon context) authenticates as postgres, which owns schema public and needs no extra grants. If you must migrate auth users, note that the auth schema is owned by supabase_auth_admin; DDL there requires that role or postgres.

How to prevent it

  • Run schema replay as postgres, not anon/authenticated. Point the DDL phase at the direct connection string that resolves to postgres. See the Supabase connection string builder to assemble the correct URI, and the service-role key note on why server-side steps bypass row-level restrictions.

  • Grant privileges up front, once. When you provision a custom migration role, issue grant usage, create on schema public to <role>; before the first CREATE TABLE rather than reacting to 42501 mid-run.

  • Set default privileges for future objects so later steps inherit access:

    alter default privileges in schema public
      grant all on tables to migrator;
    
  • Verify with has_schema_privilege before you start. A one-line check in your preflight catches the missing CREATE grant before schema replay begins, instead of failing partway through. SupaMigrate runs this preflight against the destination and connects each phase as the role that has the rights it needs.

For the underlying concepts, see schema replay and search_path. If a later step fails because the object was not created at all, that surfaces as relation does not exist.

Frequently asked questions

Why did this start after upgrading to Postgres 15?
In Postgres 15 the implicit CREATE grant on schema public was removed from the PUBLIC role. Roles that relied on that default can still USAGE the schema but can no longer create objects, so CREATE TABLE and similar DDL now raise SQLSTATE 42501. Add an explicit GRANT CREATE ON SCHEMA public.
Does the service role key avoid this error?
Server-side connections that authenticate as postgres (or the service_role) already own or have full rights on schema public, so DDL succeeds without extra grants. The error usually appears when you run migration DDL as anon or authenticated, or as a custom low-privilege role.
How do I see which role is actually running the query?
Run SELECT current_user, session_user; on the same connection that failed. The name it returns is the role you must grant USAGE and CREATE to, or the role whose DDL you must instead run as the owner.

Related errors