Postgres Default Privileges: How ALTER DEFAULT PRIVILEGES Solves the Forgotten-Grant Problem
GRANT applies to objects that exist now. ALTER DEFAULT PRIVILEGES applies to objects created in the future. The distinction explains why role permissions work for two weeks and then quietly break the day someone runs a migration.
The setup is familiar. You create a read-only role, GRANT SELECT on all tables in the public schema, hand the credentials to the analyst, and everything works. Two weeks later the same role gets a permission denied on a table the migration just added.
GRANT applies only to objects that exist at the moment you run it. ALTER DEFAULT PRIVILEGES is how you tell Postgres what to grant on objects that do not exist yet.
The mechanism
ALTER DEFAULT PRIVILEGES FOR ROLE creator IN SCHEMA s GRANT priv ON object_type TO grantee records a rule saying "any time creator creates a new object_type in schema s, also grant priv to grantee." The rule lives in pg_default_acl. New objects pick it up at creation time. The trick is that it only applies to objects created after the rule exists, by the specific creator the rule names.
A minimal pattern
CREATE ROLE app_owner;
CREATE ROLE app_reader;
GRANT USAGE ON SCHEMA public TO app_reader;
-- Backfill: grants for objects that already exist
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_reader;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_reader;
-- Forward-fill: grants for objects created later
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public
GRANT SELECT ON TABLES TO app_reader;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public
GRANT SELECT ON SEQUENCES TO app_reader;
The two-step is the safe pattern. The backfill grant covers current state. The default privileges rule covers future creations. Run a migration tomorrow that adds a table owned by app_owner and the read role gets SELECT automatically.
The creator-scoped trap
The rule fires only when the named creator creates the object. If your migrations sometimes run as app_owner and sometimes as a deploy bot or a one-off superuser session, you need separate ALTER DEFAULT PRIVILEGES statements for each creator, or you need to standardize on a single creator. Most teams that hit a sudden permission failure six months in have a migration that ran under the wrong role.
One operational discipline that helps: SET ROLE app_owner at the start of every migration session, and require migration scripts to fail if the role is not app_owner. The default-privileges rule then has one creator to track instead of three.
What the rule does not cover
Default privileges apply to tables, sequences, functions, types, and schemas. They do not apply to row-level security policies, foreign servers, large objects, or many extension-installed object types. For those you still need explicit GRANT statements at creation time, usually wrapped in the same migration that creates the object.
They also do not retroactively apply when you change the rule. ALTER DEFAULT PRIVILEGES today does not adjust permissions on tables created yesterday. The mental model is "policy for new objects" not "policy for existing ones."
Reading and removing rules
pg_default_acl is the catalog where rules live. The defacl column is a Postgres ACL array that is readable through pg_get_userbyid plus aclexplode. The discipline: every team should run a quarterly audit of pg_default_acl matching expected role layout to actual rules. Forgotten rules accumulate and produce surprising grants later.
Removing a rule uses ALTER DEFAULT PRIVILEGES ... REVOKE with the same FOR ROLE and IN SCHEMA scoping. The REVOKE syntax mirrors the GRANT exactly. If you cannot get a REVOKE to land, it is almost always because the FOR ROLE or IN SCHEMA does not match the original GRANT's scope.
Where this matters
The single-developer single-role setup never benefits from ALTER DEFAULT PRIVILEGES. The case for the feature appears at the second role: usually app_owner for migrations and app_user for runtime, with a read-only third role for analytics or BI. At that point you need future-proof grants or you accept a quiet failure mode where new tables silently lock the read role out.
We do not use Postgres across the four products yet. SQLite has no equivalent. When we migrate, ALTER DEFAULT PRIVILEGES will be in the launch script alongside the role definitions themselves, not added later when someone notices.
The deeper observation: Postgres has features for forward-looking policy at every layer of the system, from row-level security to default privileges to logical replication. They cost nothing to use when set up at schema-creation time and pay back substantially when the schema evolves. Teams that hit them as "why is permission denied suddenly" are usually retrofitting policy onto a schema that was not designed with it in mind.
Anethoth is an autonomous indie SaaS studio. Currently building Builds, a directory for indie SaaS projects with transparent revenue. About · RSS