Fix "permission denied for schema public" on PostgreSQL 15+

Fix Django migrate failing with permission denied for schema public on PostgreSQL 15 and newer: make the app role own the database or schema, grant CREATE on public, and set up roles correctly on managed databases.

Problem statement

On a new database, the first python manage.py migrate fails:

django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the
django_migrations table (permission denied for schema public
LINE 1: CREATE TABLE "django_migrations" ...

The same role worked on PostgreSQL 14. PostgreSQL 15 changed a long-standing default: ordinary users no longer have CREATE permission on the public schema. Only the database owner (and superusers) can create tables there. If your app role is not the owner of the database or schema, migrations cannot create a single table.

What changed in PostgreSQL 15PG 14 and oldereveryone may CREATEPG 15+only owner may CREATEApp rolenot the ownermigrate failspermission deniedSince PostgreSQL 15 the public schema is writable only by the database owner by default.
  1. Simplest: make the app role own the database: ALTER DATABASE myapp OWNER TO myapp; (on new databases, CREATE DATABASE myapp OWNER myapp;).
  2. Then, connected to that database, ALTER SCHEMA public OWNER TO myapp; if needed.
  3. Or grant explicitly: GRANT USAGE, CREATE ON SCHEMA public TO myapp;.
  4. On managed databases, run these as the provider's admin role.
  5. Re-run python manage.py migrate.

Step-by-step solution

1. Check owners and privileges

Connect as an admin role to the app's database:

\l myapp
\dn+ public
SELECT current_user;

\dn+ public shows the schema owner and access privileges. On PostgreSQL 15+, a fresh database shows public owned by pg_database_owner, a special role meaning "whoever owns the database", with USAGE but not CREATE granted to PUBLIC.

2. Make the app role the owner

For a database dedicated to one Django project, ownership is the cleanest fix:

ALTER DATABASE myapp OWNER TO myapp;

Because public is owned by pg_database_owner on 15+, the new database owner automatically gets full rights on public. If the schema was created differently (for example by a restore from an older version), set it explicitly while connected to myapp:

\c myapp
ALTER SCHEMA public OWNER TO myapp;

3. Or grant CREATE explicitly

If the database must stay owned by an admin role, for example due to organisational policy, grant the app role what migrations need:

\c myapp
GRANT USAGE, CREATE ON SCHEMA public TO myapp;

Grants on a schema are per database: run them while connected to the app database, not to postgres.

what a plain login role may do in publicPostgreSQL 14 and olderPUBLIC has USAGE and CREATE✓any role can create tables✕convenient, but less securePostgreSQL 15 and newerPUBLIC has USAGE only✓owner or granted roles create tablesapp role must own or be grantedOld setup scripts that only grant CONNECT now fail at the first migrationdjango-deployment.com
Update provisioning scripts to set the database owner or grant CREATE on public.

4. Managed databases

On RDS, Cloud SQL, Azure, DigitalOcean, and others, you connect as an admin role that is not a true superuser but can grant and alter ownership of objects it controls:

CREATE ROLE myapp LOGIN PASSWORD '...';
GRANT myapp TO current_user;            -- some providers require membership to change owner
CREATE DATABASE myapp OWNER myapp;

If the provider created the database for you, use ALTER DATABASE ... OWNER TO or the GRANT from step 3 as the admin user.

5. Fix existing objects after a restore

A dump restored with the original owner names may leave tables owned by another role, so later migrations fail with must be owner of table. Reassign everything:

\c myapp
REASSIGN OWNED BY old_owner TO myapp;

Use pg_restore --no-owner --role=myapp on future restores so objects are created as the app role.

provisioning a database for a Django appApp role owns the databaseCREATE DATABASE ... OWNERpublic schema owned by db owneror GRANT CREATE ON SCHEMAGrants run inside the app database\c myapp firstRestores use --no-owner --roleobjects owned by the app roleGranting only CONNECTenough on 14, fails on 15+Ownership is simpler than a growing list of grantsdjango-deployment.com
Bake these steps into the script or Terraform that creates databases.

Verification check:

python manage.py migrate
python manage.py dbshell -c "\dt django_migrations"

migrate should apply all migrations, and the table should be owned by your app role.

Explanation

Before version 15, every database's public schema granted CREATE to the PUBLIC pseudo-role, so any user able to connect could create objects that other users might accidentally use, a long-known security weakness (CVE-2018-1058 discussed the search_path risk). PostgreSQL 15 removed that default and made public owned by pg_database_owner. The change is safer, but it broke the common recipe of creating a database as postgres and granting the app user only CONNECT or ALL PRIVILEGES ON DATABASE, which never covered schema-level CREATE.

Edge cases / notes

  • GRANT ALL ON DATABASE grants database-level privileges (CONNECT, CREATE schemas, TEMP), not table creation in public. It does not fix this error.
  • Upgrades. A cluster upgraded from 14 with pg_upgrade keeps the old permissions; only newly created databases get the new default.
  • Separate migration role. Some teams run migrations as an owner role and the app as a role with only DML grants. Then also set ALTER DEFAULT PRIVILEGES so new tables are accessible to the app role.
  • Tests need CREATEDB on the role, or a TEST settings block pointing at an existing database.

Next steps

FAQ

Why did this work on PostgreSQL 14?

Older versions let every role create objects in public. Version 15 removed that default.

Is GRANT ALL PRIVILEGES ON DATABASE enough?

No. That covers database-level privileges, not creating tables inside the public schema.

Should the Django role be a superuser?

No. Owning its own database is enough and keeps the blast radius small.

Does this affect existing databases after upgrading?

Databases upgraded in place keep their old permissions. New databases on 15+ get the stricter default.

Keep going