"Permission denied for schema public" in PostgreSQL 15 and later
Posted by Kyle Hankinson August 4, 2026
If a role that worked fine for years suddenly fails like this after a Postgres upgrade or a move to a new server:
CREATE TABLE widgets (id int);
ERROR: permission denied for schema public
LINE 1: CREATE TABLE widgets (id int);
^
nothing is broken. You are on PostgreSQL 15 or later, and this is a deliberate change: ordinary roles can no longer create objects in the public schema by default. The same user, same statement, same everything succeeds on PostgreSQL 14 and fails on 15, 16, 17, and 18. I verified this directly by running an identical non-superuser CREATE TABLE against PostgreSQL 14.23 (succeeds) and 16.14 (fails with the error above).
The quick fix
If you just want the old behavior back for your application role, connect to the affected database as its owner (or a superuser) and run:
GRANT CREATE ON SCHEMA public TO app_user;
Verified on PostgreSQL 16: the CREATE TABLE that failed a second earlier succeeds immediately after the grant. USAGE on public is still granted to everyone by default, so GRANT CREATE alone is enough; add USAGE to the statement only if someone has revoked it.
A caution before you paste that into production: this reopens a shared schema. The restriction exists for a reason, so treat the grant as a conscious decision rather than a reflex, and consider the alternatives below first. Also note that GRANT ... TO PUBLIC (every role, present and future) restores the pre-15 world for the entire server one database at a time, which is almost never what you want.
What actually changed in PostgreSQL 15
Two related things, both visible in \dn+ public.
On PostgreSQL 14, the default looks like this:
Name | Owner | Access privileges | Description
--------+----------+----------------------+------------------------
public | postgres | postgres=UC/postgres+| standard public schema
| | =UC/postgres |
The line =UC/postgres means every role (= is the PUBLIC pseudo-role) holds both USAGE and CREATE. Anyone who can connect can create tables.
On PostgreSQL 16, the same command shows:
Name | Owner | Access privileges | Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
| | =U/pg_database_owner |
Two differences. First, =U/...: everyone still has USAGE (you can reference objects in public), but CREATE is gone. Second, the schema is now owned by pg_database_owner, a pseudo-role that always resolves to whoever owns the current database, instead of being owned by the bootstrap superuser.
The motivation was security. With a world-writable default schema, any user could create objects (including operators and functions) that other users' queries might resolve first via search_path, the attack pattern behind CVE-2018-1058. The change is listed in the PostgreSQL 15 release notes, and the reasoning is laid out in the schemas documentation.
You can check what a role can actually do without reading privilege strings:
SELECT has_schema_privilege('app_user', 'public', 'CREATE') AS can_create,
has_schema_privilege('app_user', 'public', 'USAGE') AS can_usage;
On a default PostgreSQL 16 database this returns f and t for a plain role, which is the whole story of the error in one row.
Three fixes, and when to use each
| Fix | One-liner | Best for |
|---|---|---|
| Grant CREATE on public | GRANT CREATE ON SCHEMA public TO app_user; |
Restoring old behavior quickly, dev boxes, CI |
| Dedicated schema | CREATE SCHEMA app AUTHORIZATION app_user; |
Multi-app or multi-tenant databases |
| Make the role the owner | ALTER DATABASE appdb OWNER TO app_user; |
A database that exists solely for this application |
The dedicated schema is usually the right call for anything shared. The role owns its schema outright and needs no grants on public at all:
CREATE SCHEMA app AUTHORIZATION app_user;
Unqualified table names will still resolve to public by default, so either qualify names (CREATE TABLE app.gadgets ...) or point the role's search path at its schema:
ALTER ROLE app_user IN DATABASE appdb SET search_path = app;
Verified on PostgreSQL 16: after that ALTER ROLE, an unqualified CREATE TABLE from app_user lands in the app schema.
The ownership route fits the common one-database-per-app layout. Because public now belongs to pg_database_owner, making your application role the database owner gives it full rights over public with no explicit grants (verified on 16: a fresh database, one ALTER DATABASE ... OWNER TO, and the role creates tables in public immediately). Cleaner still is creating it correctly from the start with CREATE DATABASE appdb OWNER app_user. If you decide to rebuild an existing database under the right owner and connections are in the way, see how to drop a database with active connections.
The gotchas that keep this error alive
Grants are per database. GRANT CREATE ON SCHEMA public fixes only the database you were connected to when you ran it. Every database has its own public schema with its own ACL. I verified this on PostgreSQL 16: after granting in appdb, the same role creating a table in appdb2 still fails with permission denied for schema public. If your app spans several databases, repeat the grant in each one. This is worth remembering in a GUI client too: SQLPro Studio shows each database's schemas and owners in its object browser, which makes it easy to confirm against which database a grant actually ran before you assume the problem is solved.
Old tutorials predate the change. Most "create a Postgres user for your app" guides written before late 2022 end with CREATE ROLE ... LOGIN and nothing else, because nothing else was needed. On 15+ that recipe produces a user who can connect and read but cannot create anything.
Restores trip it too. Dump a pre-15 database, restore into 15+, and objects owned by non-owner roles or scripts that recreate tables in public can hit the error mid-restore. Run the restore as the database owner, or apply one of the fixes above first.
Managed providers vary. On services like RDS or Cloud SQL you typically do not get a true superuser; use the admin account the provider gives you (which owns the databases it creates) to run the grants. Some providers pre-configure public more permissively, so check \dn+ public before assuming either behavior.
The GRANT syntax has more depth (granting to groups, WITH GRANT OPTION, default privileges for future objects) than one article can cover; the GRANT reference is the authoritative list. For most teams, though, the decision is simply the table above: reopen public, give the app its own schema, or make the app the owner. Any of the three turns the error into a one-time footnote of the PostgreSQL 15 upgrade.
About the author — Kyle Hankinson is the founder and sole developer of SQLPro Studio and the Hankinsoft Development suite of database tools. He has been building native macOS and iOS applications since 2010.
Try SQLPro Studio — A powerful database manager for MySQL, PostgreSQL, Microsoft SQL Server, SQLite, Oracle, and Snowflake. Available on macOS, iOS, and Windows.
Download Free Trial View Pricing Compare