Now-Next

SaaS architecture

permission denied for schema public (PostgreSQL 15+)

Why CREATE TABLE fails with permission denied for schema public on PostgreSQL 15 to 18 and not on 14, measured, and which of five fixes work.

Chris van Eijk, Now-Next · · 6 min read

ERROR: permission denied for schema public (SQLSTATE 42501) on a CREATE TABLE means the role has no CREATE privilege on the public schema. Since PostgreSQL 15 that is the default for every new database: public belongs to the database owner, and everyone else may only use it. An application role that could create its tables on PostgreSQL 14 gets this error on a fresh 15, 16, 17 or 18 database — and not on a database restored from 14 (or, per the release notes, upgraded with pg_upgrade), which keeps the old permissions.

We measured the same steps on PostgreSQL 14.24, 15.19, 16.15, 17.11 and 18.6, with an ordinary login role app in a database created by postgres. Versions 15 to 18 gave identical results.

What changed in PostgreSQL 15?

Two defaults, both in the release notes of 15.0: “Remove PUBLIC creation permission on the public schema” and “Change the owner of the public schema to be the new pg_database_owner role”. In our containers:

PostgreSQL 14PostgreSQL 15, 16, 17, 18
owner of public in a new databasepostgrespg_database_owner
privileges on public{postgres=UC/postgres,=UC/postgres}{pg_database_owner=UC/pg_database_owner,=U/pg_database_owner}
create table t(id int) as appCREATE TABLEERROR: 42501: permission denied for schema public
create view, create sequence as appcreatedthe same error

=UC is USAGE and CREATE for everyone; =U is usage only. pg_database_owner cannot be granted (role "pg_database_owner" cannot have explicit members); its member is the current database’s owner, and a role that inherits from that owner has its privileges too. The release notes add that “Upgrading a cluster or restoring a database dump will preserve public’s existing permissions”, and that held when we dumped a database made on 14 and restored it into 17: the dump contained ALTER SCHEMA public OWNER TO postgres; and GRANT ALL ON SCHEMA public TO PUBLIC;, and after the restore app could create tables again. A database dumped from a fresh 15 kept the 15 default after restoring into 17. So the same application can work against the old database and fail against a new one on the same server.

The template matters too. A new database copies public’s privileges from its template: when we granted CREATE on public to PUBLIC in template1 on 17, the next create database had =UC and app could create tables, while create database … template template0 still had the 15 default.

With the official Docker image you may never see the error at first: POSTGRES_USER becomes a superuser and the owner of POSTGRES_DB. In a container started with POSTGRES_USER=app, app created tables; a second, ordinary role created afterwards got permission denied for schema public. That second role, or a managed database where you are not a superuser, is where migration tools hit it on their first table. The same image also explains role "postgres" does not exist: with POSTGRES_USER=app there is no postgres role at all, see role does not exist.

Which message do you get, and what is it missing?

The message depends on which privilege is missing and on whether the statement names the schema. We revoked privileges one at a time:

What app got on PostgreSQL 15 to 18, by what it lacked on public

What does the role have on the schema?

  • USAGE, no CREATE (the default since 15)permission denied for schema public on createReading a table works if the role has SELECT on it; without, the message names the table: permission denied for table pub_t.
  • No USAGE, statement says public.pub_tpermission denied for schema publicFor reads too, even with SELECT granted on the table.
  • No USAGE, statement says only pub_tNo permission error at allA read gave 42P01: relation "pub_t" does not exist; a create gave 3F000: no schema has been selected to create in.

PostgreSQL 15.19, 16.15, 17.11 and 18.6 gave the same message in each case. On 14.24 every read and the unqualified create did too; only create table public.pub_t succeeded, because PUBLIC still had CREATE and creating needs no USAGE. Superusers and members of pg_read_all_data or pg_write_all_data keep USAGE on every schema, so the last two cases do not apply to them; neither role grants CREATE. Each case in a fresh database created by postgres.

The last case is the misleading one: without USAGE, public drops out of the role’s search path, so an unqualified name finds nothing and the error says the table does not exist, while it does. If a role gets relation does not exist for a table you can see, check has_schema_privilege('app', 'public', 'USAGE') before you check the spelling. The other causes of that message, and one query that tells them apart, are in relation does not exist in PostgreSQL.

at character 14, which appears in server logs and many reports of this error, is the position in the statement where the problem starts. It was in our server log as permission denied for schema public at character 14 for create table t(id int): create table is 13 characters, so character 14 is the table name (with if not exists it was 28). psql shows the same position as a caret under the line. For create view and create sequence the server gave no position.

Which fixes work?

Each in a fresh database, as postgres, followed by create table t(id int) as app:

FixPostgreSQL 15–18Note
grant create on schema public to appworksthe pre-15 behaviour, for one role
create schema app authorization appworks: the table landed in appthe default search_path is "$user", public, so a schema named after the role needs no setting; create table public.t still refused
alter database mydb owner to appworksapp becomes a member of pg_database_owner, so it owns public
alter schema public owner to appworks
grant all privileges on database mydb to appdoes not work: still permission denied for schema publicit allowed create schema (the database’s CREATE privilege), not tables in public

GRANT ALL PRIVILEGES ON DATABASE is a common first attempt, and the reason it fails is that database privileges are a different list: CONNECT, CREATE (new schemas) and TEMPORARY. That is also why create temp table worked for app in every version without any grant — until we ran revoke temp on database q4 from public, after which it got permission denied to create temporary tables in database "q4". Which privilege each statement needs on tables, sequences and schemas is in permission denied for table in PostgreSQL.

The PostgreSQL documentation recommends the second row as a “secure schema usage pattern”: “Constrain ordinary users to user-private schemas”, and adds that “In PostgreSQL 15 and later, the default configuration supports this usage pattern.” For a SaaS application with one migration role, that means: let the migration role own its own schema, grant the runtime role USAGE on it and the table privileges it needs, and set the search path of both roles to that schema (alter role app_runtime set search_path = core) or qualify every name. The default "$user", public only finds a schema named after the role itself: with a schema core owned by the migration role, the migration role’s unqualified create table still went to public and failed, and the runtime role got relation "t" does not exist until it used core.t. Leave public alone.

Granting CREATE on public to the application role works too; an upgraded database goes further and gives it to PUBLIC, every role. The cost: a role with that grant can put objects in a schema on everyone’s search path. When app created a function public.upper(varchar), another role’s upper('abc'::varchar) called app’s function instead of the built-in one. Handing over the database is the broadest option: the owner of a database can also drop it, even without CREATEDB, and the documentation calls its pattern secure “unless an untrusted user is the database owner”. Making app the owner of public likewise lets it drop other roles’ tables there, as measured in must be owner of table.

If the runtime role is not the role that created the tables, the next error after this one is usually permission denied for table on tables created later. That is the default-privileges problem measured in ALTER DEFAULT PRIVILEGES not working.

What we did not measure

pg_upgrade from 14 to 15 (we measured dump and restore, which the release notes treat the same). Managed services that create databases with their own defaults. Supabase, where the same message usually comes from the anon and authenticated roles and the API, which is a different setup.

Sources: PostgreSQL 15.0 release notes, migration section; PostgreSQL 17 documentation on schemas and usage patterns. Both read on 21 October 2026. All measurements are our own, on PostgreSQL 14.24, 15.19, 16.15, 17.11 and 18.6 in containers, on 21 October 2026.

Let's talk

What are we building?

One conversation is enough to know whether we fit. Tell us what you have in mind — we will tell you how fast it can happen, and whether we are the right people for it.

Start the conversation