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:
-
Postgres 15 removed the implicit CREATE grant. Before Postgres 15, the built-in
PUBLICrole hadCREATEon schemapublic, so almost any role could runCREATE TABLE. Postgres 15 dropped that default. A role that migrated fine on PG 14 now fails the moment schema replay runs its firstCREATE TABLE. -
Running DDL as
anonorauthenticated. These Supabase roles are meant for row-level API access, not schema changes. They typically haveUSAGEonpublicbut notCREATE, and no rights at all onauth,storage, or other reserved schemas. If your connection string or pooler session resolves to one of these roles, DDL fails. -
The target object is owned by another role.
ALTER TABLE,DROP, orCREATE INDEXon an existing object requires ownership (or superuser). If the destination already has objects owned bypostgresand you connect as a different role, you get42501on the alter even though you canSELECTthe 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, notanon/authenticated. Point the DDL phase at the direct connection string that resolves topostgres. 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 firstCREATE TABLErather than reacting to42501mid-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_privilegebefore you start. A one-line check in your preflight catches the missingCREATEgrant 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