SaaS architecture
permission denied to create extension in PostgreSQL
Which extensions a non-superuser may create in PostgreSQL 16 to 18, measured: pgcrypto, citext and uuid-ossp with one grant; vector, PostGIS, dblink not at all.
ERROR: permission denied to create extension "vector" (SQLSTATE 42501) means the role running CREATE EXTENSION is not allowed to install that extension, and the HINT line under it says which of two reasons applies. Must be superuser to create this extension means the extension is not marked trusted: only a superuser can install it, and no grant changes that. Must have CREATE privilege on current database to create this extension means it is trusted, and one grant on the database is enough. pgvector and PostGIS are in the first group, pgcrypto, citext and uuid-ossp in the second.
We tried eight extensions as an ordinary login role app on PostgreSQL 16.15, 17.11 and 18.6, plus the official pgvector image (pgvector 0.8.7 on 17.11) and the PostGIS image (PostGIS 3.5.2 on 17.5). The table came out the same on every version.
Which extensions can a non-superuser create?
Each case in a fresh database owned by postgres, unless the column says otherwise:
| Extension | Trusted | No CREATE on the database | grant create on database … to app | app owns the database |
|---|---|---|---|---|
citext | yes | refused, hint Must have CREATE privilege | created | created |
pgcrypto | yes | refused, hint Must have CREATE privilege | created | created |
uuid-ossp | yes | refused, hint Must have CREATE privilege | created | created |
hstore, pg_trgm, btree_gist, unaccent, cube | yes | — | created | — |
vector (pgvector 0.8.7) | no | refused, hint Must be superuser | refused | refused |
postgis 3.5.2 | no | refused, hint Must be superuser | refused | refused |
pg_stat_statements | no | refused, hint Must be superuser | refused | refused |
dblink | no | refused, hint Must be superuser | refused | refused |
postgres_fdw | no | refused, hint Must be superuser | refused | refused |
earthdistance | no, although cube, which it requires, is | — | refused, also with cascade | — |
You can read the trusted flag before you try: select name, version, superuser, trusted from pg_available_extension_versions where name = 'vector';. The control files of pgvector and PostGIS have no trusted line, so they default to false. Of the contrib extensions in the official images, the same 21 were trusted on 16, 17 and 18: btree_gin, btree_gist, citext, cube, dict_int, fuzzystrmatch, hstore, intarray, isn, lo, ltree, pg_trgm, pgcrypto, plpgsql, seg, tablefunc, tcn, tsm_system_rows, tsm_system_time, unaccent and uuid-ossp. create extension earthdistance cascade installed the trusted cube first and then failed with Must be superuser, which rolled cube back too. postgis_topology creates its own schema, so without CREATE on the database it failed earlier, with permission denied for database and no hint; with the grant it got Must be superuser like PostGIS itself.
CREATE EXTENSION goes through, on PostgreSQL 16 to 18Is the extension already installed in this database?
- Yes, and the statement says
IF NOT EXISTSGoes through, whatever the role may doNOTICE: extension "vector" already exists, skipping— also for an untrusted extension and without any grant. It does not update an older installed version. - No, and it is marked trustedNeeds
CREATEon the database, nothing moreNotCREATEon the schema it goes into: it went into a schema whereapphad neitherUSAGEnorCREATE— andappthen could not use it there. - No, and it is not trustedOnly a superuserOwning the database does not help. A superuser installs it once; after that the role can use it, given
USAGEon its schema.
Measured on PostgreSQL 16.15, 17.11 and 18.6 with citext, pgcrypto, uuid-ossp, dblink, postgres_fdw and pg_stat_statements; pgvector 0.8.7 and PostGIS 3.5.2 on 17.
Why does a trusted extension not need CREATE on the schema?
Because the installation script does not run as you. The documentation of the trusted parameter: installation “will be permitted for anyone who has CREATE privilege on the current database”, and then “the installation or update script is run as the bootstrap superuser, not as the calling user”. We saw that in two places. On PostgreSQL 15 and later, where an ordinary role has no CREATE on public (measured in permission denied for schema public), app still installed citext into public with only the database grant. And after app installed pgcrypto and citext, each extension itself belonged to app (pg_extension.extowner), but every object in it belonged to postgres: pgcrypto’s 36 functions (37 on 18), and citext’s 47 functions, 26 operators and its type. The schema check that was skipped at install time still applies afterwards: citext installed into a schema where app had no USAGE gave app permission denied for schema locked as soon as it used the type.
That ownership split has a consequence for migrations. app could drop extension and alter extension … update the extensions it had created; for an extension a superuser had created, even as owner of the database, it got must be owner of extension pgcrypto. And ALTER EXTENSION … OWNER TO does not exist (syntax error at or near "owner"), so you cannot hand an extension over afterwards.
How do you use vector or PostGIS without superuser?
Have a superuser create it once per database, and let the migration say IF NOT EXISTS:
-- as a superuser, once per database
create extension if not exists vector;
-- in the application's migration, as app
create extension if not exists vector;
-- NOTICE: extension "vector" already exists, skipping
The second statement is the useful part: create extension if not exists by app gave a notice and no error when the extension was already there, also for untrusted extensions and also without CREATE on the database. Without IF NOT EXISTS it failed with extension "vector" already exists (42710). So a migration that declares its extensions with IF NOT EXISTS runs as an ordinary role on any database where they have been installed, and fails with the permission error, as it should, where they have not. It also skips when the installed version is older: with citext 1.4 installed by a superuser, create extension if not exists citext version '1.6' by app gave the same notice and left 1.4 in place, and alter extension citext update got must be owner of extension citext. Updating an extension stays a superuser job.
Two variations we measured:
- An
extensionsschema. A superuser created uuid-ossp in a schemaextensions. WithoutUSAGEon that schema,appgotpermission denied for schema extensions; withgrant usage on schema extensions to app,extensions.uuid_generate_v4()worked, and the unqualified name only afterset search_path = public, extensions(before that:function uuid_generate_v4() does not exist). - Not needing the extension at all.
gen_random_uuid()is built in since PostgreSQL 13 and worked without pgcrypto or uuid-ossp.crypt()andgen_salt()are not: without pgcryptoappgotfunction gen_salt(unknown) does not exist; after a superuser created pgcrypto,crypt('x', gen_salt('bf'))returned a 60-character bcrypt hash forapp.
Why does pg_stat_statements still fail after CREATE EXTENSION?
Because it needs a library loaded at server start, which CREATE EXTENSION does not do. As a superuser, create extension pg_stat_statements succeeded, and the first select * from pg_stat_statements failed with pg_stat_statements must be loaded via "shared_preload_libraries" (55000) — on 16.15 the same text without the quotes. The fix is shared_preload_libraries = 'pg_stat_statements' in the server configuration and a restart; no grant gets around it.
What we did not measure
Managed services such as Amazon RDS, Cloud SQL and Supabase, which give their admin role a way to install a list of extensions without being a real superuser; there, their own documentation decides. Extensions installed from source with a custom control file. postgis_raster.
Sources: PostgreSQL 17 documentation on packaging related objects into an extension (the trusted parameter), read on 21 October 2026. All measurements are our own, on PostgreSQL 16.15, 17.11 and 18.6, pgvector 0.8.7 and PostGIS 3.5.2 in containers, on 21 October 2026.