GRANT/REVOKE Patterns
PostgreSQL privileges are explicit by default. New objects do not automatically grant access to application roles. Model schema, table, column, and sequence grants deliberately.
Search across all documentation pages
PostgreSQL privileges are explicit by default. New objects do not automatically grant access to application roles. Model schema, table, column, and sequence grants deliberately.
-- Application read path
GRANT USAGE ON SCHEMA app TO app_readers;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_readers;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA app TO app_readers;
-- Application write path (narrower)
GRANT USAGE ON SCHEMA app TO app_writers;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_writers;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_writers;When to reach for this: Every new schema, every migration that creates tables, and every new microservice role.
CREATE SCHEMA app AUTHORIZATION app_owner;
CREATE ROLE app_api LOGIN PASSWORD 'vault';
CREATE ROLE app_ro LOGIN PASSWORD 'vault';
GRANT app_readers TO app_ro;
GRANT app_writers TO app_api;
-- Table-level with column restriction
CREATE TABLE app.users (
id bigint PRIMARY KEY,
email text NOT NULL,
ssn_last4 char(4) -- sensitive
);
GRANT SELECT (id, email) ON app.users TO app_ro;
GRANT SELECT, INSERT, UPDATE ON app.users TO app_api;
REVOKE ALL ON app.users FROM PUBLIC;
-- Verify effective grants
SELECT grantee, privilege_type, column_name
FROM information_schema.column_privileges
WHERE table_schema = 'app' AND table_name = 'users';What this demonstrates:
USAGE is required before table accessGRANT SELECT (cols) hides sensitive fields from analystsREVOKE ALL FROM PUBLIC closes the default-public hole on new clusters (when hardening)| Object | Common privileges | Notes |
|---|---|---|
| DATABASE | CONNECT, CREATE, TEMP | CREATE on DB allows new schemas |
| SCHEMA | USAGE, CREATE | CREATE allows objects in schema |
| TABLE | SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER | TRUNCATE is separate from DELETE |
| SEQUENCE | USAGE, SELECT, UPDATE | UPDATE needed for nextval |
| FUNCTION | EXECUTE | Required for RPC from app |
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON DATABASE myapp FROM PUBLIC;
GRANT CONNECT ON DATABASE myapp TO app_readers;PUBLIC historically on public schema.PUBLIC and grant explicitly to group roles.ALTER DEFAULT PRIVILEGES handles future objects (see dedicated page).app_owner NOLOGIN; runtime connects as app_api.REASSIGN OWNED moves ownership during role renames.permission denied for schema app despite table GRANT. Fix: GRANT USAGE ON SCHEMA app.INSERT works with serial until sequence exhausted on new row. Fix: GRANT USAGE, SELECT ON SEQUENCE.ALTER DEFAULT PRIVILEGES for migrator role.SELECT * and fails on hidden columns inconsistently. Fix: Explicit column lists in app queries.| Alternative | Use When | Don't Use When |
|---|---|---|
| Row-Level Security | Tenant isolation in shared schema | Single-tenant simple CRUD |
| Views as security barrier | Hide columns/joins from base tables | High write volume through views |
SECURITY DEFINER functions | Controlled elevated operations | Broad app write access |
Covers current tables only. Sequences, functions, and future tables need separate grants or default privileges.
information_schema.table_privileges, has_table_privilege(), or GUI tools. Include grants in migration SQL reviews.
Rare for tables. DROP OWNED BY is the bulk cleanup tool when decommissioning roles.
Referencing side needs REFERENCES privilege on referenced table; both schemas need USAGE.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 18, 2026