PostgreSQL

Postgres "Permission Denied for Schema Public" After PostgreSQL 15 Update

"Permission denied for schema public" appearing right after upgrading to PostgreSQL 15 isn't a bug or a misconfiguration you introduced — it's the direct, expected result of a deliberate security change PostgreSQL 15 made to the default privileges on the public schema for newly created databases.

The Problem

An application or user that previously had no trouble creating tables or other objects suddenly fails after a PostgreSQL upgrade:

ERROR:  permission denied for schema public
LINE 1: CREATE TABLE orders (id SERIAL PRIMARY KEY);

The exact same operation worked fine before the upgrade, with no application code changes, which correctly points at the database upgrade itself as the direct cause.

Why It Happens

Starting with PostgreSQL 15, newly created databases no longer grant the CREATE privilege on the public schema to the PUBLIC role (meaning, effectively, to every user) by default — in earlier PostgreSQL versions, any authenticated user could create objects in the public schema without needing explicit permission granted. This was changed specifically as a security hardening measure, since the previous default allowed any user with database access to create arbitrary objects, which was considered an unnecessarily broad default permission for many use cases. This affects:

  • Newly created databases on PostgreSQL 15+, which get this more restrictive default from the start
  • Existing databases upgraded to PostgreSQL 15+ may or may not be affected depending on the exact upgrade path and whether the upgrade process preserved the previous permission state or applied the new default — behavior here can vary based on how the upgrade was performed
  • Any user or role that isn't the database owner (who retains full privileges on schemas they own by default) and doesn't have this specific privilege explicitly granted

The Fix

Explicitly grant the CREATE privilege on the public schema to the specific role(s) that need it, restoring the functional equivalent of the pre-15 default behavior for your application's actual needs:

GRANT CREATE ON SCHEMA public TO your_app_role;

For a broader grant matching the old default behavior more closely (allowing any authenticated user to create in the public schema, if that's genuinely what you want rather than a more scoped, deliberate grant):

GRANT CREATE ON SCHEMA public TO PUBLIC;

Be aware that explicitly restoring the old, more permissive default specifically undoes the security improvement PostgreSQL 15 introduced — consider whether a more scoped grant to only the specific roles that genuinely need object-creation privileges is a better long-term choice than broadly restoring the old behavior for every user, even if it takes a bit more upfront configuration to get right.

If your application connects using a dedicated role (which is generally good practice, rather than connecting as a database superuser), confirm that specific role has the grant, rather than granting it to a different role than the one actually used by your application's connection string:

SELECT current_user;
-- confirm this matches the role you actually granted CREATE to

For a longer-term, more explicit approach that doesn't rely on the public schema's default permissions at all, consider creating a dedicated, explicitly-named schema for your application's objects, with clear, intentional ownership and privileges — this sidesteps ambiguity about default schema permissions entirely, and is generally considered better practice than relying on the public schema's shared, ambient behavior:

CREATE SCHEMA app_schema AUTHORIZATION your_app_role;

Update your application's connection configuration or search_path to use this new, dedicated schema instead of relying on public, giving your application clearer, more intentional ownership over its own database objects going forward.

Still Not Working?

If you've granted the privilege but the error persists, confirm you granted it on the correct database — public schema privileges are scoped per-database, not globally across an entire PostgreSQL server instance, so a grant applied while connected to the wrong database has no effect on the one your application actually uses:

\c your_actual_database_name
GRANT CREATE ON SCHEMA public TO your_app_role;

The \c command in psql explicitly switches your current connection to the specified database before running the grant, ensuring it applies to the database you actually intend, rather than whatever database your session happened to be connected to by default.