createrole_self_grant is small, recent (PostgreSQL 16), and almost impossible to explain in isolation. To say what it does, we have to talk about what the role system did before 16, what it does now, and why the change happened. The parameter is one of the visible artifacts of a fairly substantial overhaul, and that overhaul is more interesting than the parameter itself.
The old role model
Before 16, CREATEROLE was, in practice, a near-superuser privilege wearing a smaller hat. A role with CREATEROLE could create, alter, and drop any non-superuser role in the cluster, including ones it had nothing to do with — including, say, the role your application connects as. It could change passwords. It could grant role memberships freely. And because role inheritance was a property of the role rather than the grant, the membership graph was a blunt instrument: either a role inherited all privileges of its groups or it inherited none, and you couldn’t say “yes for these, no for those.”
The intent was to give administrators a way to delegate user management without handing out full superuser. The result, as Robert Haas put it in the blog post announcing the v16 work, was an attribute that “was very nearly a superuser-equivalent privilege.” If you had CREATEROLE, you could create a role, give it whatever attributes you liked, log in as it, and inherit its powers — including, if you were creative, the powers of much more privileged roles.
What v16 changed
Three things, in roughly increasing subtlety.
First, CREATEROLE was cut down. A CREATEROLE user can now only modify roles it has explicit ADMIN OPTION on. It can still create new roles, but it can’t reach over and reset the password of app_admin just because it has the attribute. The attribute is now a license to manage your own roles, not everyone’s.
Second, inheritance moved from the role to the grant. Pre-16, ROLE alice INHERIT meant alice inherits privileges from every group she’s in, full stop. Post-16, each GRANT role TO alice carries its own INHERIT/NOINHERIT and SET/NOSET options. You can be a member of dba_group with SET but not INHERIT (meaning you can SET ROLE dba_group deliberately but don’t get its powers automatically), and a member of read_all with both. The membership graph finally has the granularity it should always have had.
Third, and this is where today’s parameter walks in: a CREATEROLE user who creates a new role doesn’t automatically own anything about that role. Without something else happening, they’d create a role they have no power over — they couldn’t drop it, change its password, or grant it anything. That’s a usability cliff.
What the parameter does
createrole_self_grant is the bridge. It’s a comma-separated list of grant options — legal values are set, inherit, both, or empty — that get automatically applied when a non-superuser CREATEROLE user creates a new role. The default is empty: no automatic grant.
Set it to 'set, inherit' and every role you create is automatically GRANTed back to you with both options. You can SET ROLE to it, you inherit its privileges, and you have ADMIN OPTION because you created it — which means you can drop it, alter it, grant it elsewhere. Functionally, you have full control over the roles you create, the way CREATEROLE users always assumed they did. Set it to 'set' alone and you can administer the role but don’t pick up its privileges in your normal session. Leave it empty and you get nothing, which is the strict secure default.
Context is user, so it can be set per-session, per-role, or per-database. The natural place is per-role: ALTER ROLE app_admin SET createrole_self_grant = 'set, inherit'; for the role you actually want managing application users.
What to actually do with it
The honest framing: if you’re running a normal application database with one or two CREATEROLE users managing application accounts, set it to 'set, inherit' on those roles and move on. That gives them the workflow they had pre-16 — create a role, own it, manage it — with the difference that they can no longer reach across the cluster to mess with roles they didn’t create. That’s the actual security win of the v16 redesign, and createrole_self_grant is what makes the win usable.
If you’re a managed-PostgreSQL provider, or running a multi-tenant setup where different CREATEROLE users are supposed to be isolated from each other, the more conservative 'set' or empty values become attractive. The cloud providers — RDS, Azure, Cloud SQL — have all had to think carefully about this since 16, and the Azure case in particular has been instructive about what goes wrong when the parameter isn’t set the way the provider’s tooling assumes. Leaving it at empty when your tooling expects 'set, inherit' produces roles nobody can manage. Setting it to 'set, inherit' when your security model assumes isolation produces a different problem.
Set it deliberately, scoped to the role doing the creating, and document why. The default of empty is correct for a cluster being administered by a superuser who hasn’t thought about it; it’s almost always wrong for any role that’s supposed to be managing users.