PostgreSQL Access & Privileges

A comprehensive guide to roles, grants, schemas, and security in PostgreSQL 14–16

1. Roles vs Users

In PostgreSQL, there is no real difference between a role and a user. A USER is simply a ROLE that has the LOGIN attribute. Both are managed with the same commands internally.

-- These two are exactly equivalent
CREATE USER alice WITH PASSWORD 'secret';
-- is the same as:
CREATE ROLE alice WITH LOGIN PASSWORD 'secret';

Roles can:

ROLE — base concept (no LOGIN by default)
├── Group Role — used to bundle permissions, no login
└── User Role — has LOGIN, can connect
└── Can be MEMBER of group roles → inherits their privileges

2. Role Attributes

Role attributes define what the role is allowed to do at the system level — not what database objects it can access. These are configured at creation or with ALTER ROLE.

AttributeDefaultWhat it controls
LOGIN / NOLOGINNOLOGINCan the role connect to the database server?
SUPERUSER / NOSUPERUSERNOSUPERUSERBypasses ALL access checks. Extremely dangerous to hand out.
CREATEDB / NOCREATEDBNOCREATEDBCan the role create new databases?
CREATEROLE / NOCREATEROLENOCREATEROLECan the role create, alter, or drop other roles?
INHERIT / NOINHERITINHERITDoes the role automatically inherit privileges from roles it belongs to?
REPLICATION / NOREPLICATIONNOREPLICATIONCan the role initiate streaming replication?
BYPASSRLS / NOBYPASSRLSNOBYPASSRLSCan the role bypass Row-Level Security policies?
CONNECTION LIMIT n-1 (unlimited)Maximum concurrent connections for this role.
PASSWORDnullLogin password (stored hashed).
VALID UNTILnever expiresPassword expiry timestamp.
⚠️ Critical Misconception
Attributes like NOSUPERUSER, NOCREATEDB, NOCREATEROLE only restrict system-level operations. They have zero effect on access to tables, schemas, or other database objects. Object access is controlled entirely by GRANT and REVOKE.
-- Modify attributes after creation
ALTER ROLE alice CREATEDB;
ALTER ROLE alice CONNECTION LIMIT 10;
ALTER ROLE alice VALID UNTIL '2025-12-31';
ALTER ROLE alice NOLOGIN;          -- demote to a group role
ALTER ROLE alice PASSWORD 'newpass';
ALTER ROLE alice RENAME TO alice_old;

3. The PUBLIC Role

PUBLIC is a special built-in pseudo-role. Every role automatically belongs to PUBLIC, and any privilege granted to PUBLIC applies to all current and future users in the cluster.

🚨 PostgreSQL's Permissive Defaults — Granted to PUBLIC Out of the Box
This is why simply setting restrictive role attributes is not enough — you must also REVOKE from PUBLIC.

Verifying what PUBLIC has

-- Check database-level privileges (look for "=" entries = PUBLIC)
\l your_database

-- Check schema-level privileges
\dn+ public

-- In the output:
--   =  (before the slash) means PUBLIC
--   C  = CREATE
--   U  = USAGE
--   T  = TEMP (on database)
--   c  = CONNECT (on database)
-- Example: =UC means PUBLIC has USAGE and CREATE on the schema

Locking down PUBLIC (recommended for production)

-- Prevent all users from connecting without explicit grant
REVOKE CONNECT ON DATABASE mydb FROM PUBLIC;

-- Prevent all users from creating tables (most important)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

-- Prevent temp table creation (optional, stricter)
REVOKE TEMP ON DATABASE mydb FROM PUBLIC;

-- Prevent calling all functions (optional, strictest)
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA public FROM PUBLIC;

4. INHERIT vs NOINHERIT

When a role is a member of another role, the INHERIT attribute controls whether its privileges are automatically applied.

SettingBehavior
INHERIT Default Privileges from all member roles are automatically active. The user can use them immediately without any extra step.
NOINHERIT Privileges from member roles are NOT active automatically. The user must run SET ROLE role_name to explicitly activate them.
-- Setup a group role with SELECT
CREATE ROLE readers;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readers;

-- alice has INHERIT (default): privileges work immediately
CREATE USER alice WITH LOGIN PASSWORD 'pass';  -- INHERIT is default
GRANT readers TO alice;
-- alice can SELECT right away ✓

-- bob has NOINHERIT: must SET ROLE first
CREATE USER bob WITH LOGIN PASSWORD 'pass' NOINHERIT;
GRANT readers TO bob;
-- bob gets "permission denied" here ✗
SELECT * FROM orders;

-- bob must explicitly switch into the role
SET ROLE readers;
SELECT * FROM orders; -- now works ✓
RESET ROLE;           -- back to bob's own privileges

-- Fix bob to use INHERIT instead
ALTER USER bob INHERIT;
ℹ️ When is NOINHERIT useful?
NOINHERIT is sometimes used for high-privilege roles (like a DBA role) where you want the user to consciously switch into elevated permissions — similar to sudo in Linux. For regular app users, always use INHERIT.

5. The Privilege Check Chain

PostgreSQL checks privileges at every level. A grant at one level does not bypass checks at others. You must satisfy all levels in the chain.

Step 1 — Can the role CONNECT to the database?
↓ yes → continue / no → "FATAL: role not permitted to log in"
Step 2 — Does the role have USAGE on the schema?
↓ yes → continue / no → "ERROR: permission denied for schema"
Step 3 — Does the role have SELECT/INSERT/... on the table?
↓ yes → continue / no → "ERROR: permission denied for table"
Step 4 (if RLS) — Does a row-level policy allow this row?
↓ yes → ✓ query succeeds / no → row is silently filtered or blocked
⚠️ Common Mistake
Granting SELECT ON TABLE orders means nothing if the user doesn't also have USAGE ON SCHEMA public. You always need both.

6. Database-Level Privileges

These control access at the database level, granted with GRANT ... ON DATABASE.

PrivilegeWhat it allowsDefault
CONNECTConnect to the database.Granted to PUBLIC
CREATECreate new schemas within the database.Not granted
TEMP / TEMPORARYCreate temporary tables in the database.Granted to PUBLIC
GRANT CONNECT ON DATABASE mydb TO alice;
GRANT CREATE ON DATABASE mydb TO alice;    -- allows creating new schemas
GRANT TEMP ON DATABASE mydb TO alice;      -- allows temporary tables
GRANT ALL ON DATABASE mydb TO admin_user;  -- all of the above

REVOKE ALL ON DATABASE mydb FROM alice;    -- remove everything

7. Schema-Level Privileges

Schemas are namespaces inside a database. You need schema privileges before you can access any objects within a schema.

PrivilegeWhat it allowsDefault
USAGELook up and access objects in the schema. Required to query any table in the schema.Granted to PUBLIC on public
CREATECreate new objects (tables, views, functions, sequences) in the schema.Granted to PUBLIC on public
GRANT USAGE ON SCHEMA public TO alice;     -- can use objects in schema
GRANT CREATE ON SCHEMA public TO alice;    -- can create tables/views etc.
GRANT USAGE, CREATE ON SCHEMA public TO alice;

-- Revoke CREATE from everyone (critical for production lockdown)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

-- Create a private schema only certain roles can access
CREATE SCHEMA private;
REVOKE ALL ON SCHEMA private FROM PUBLIC;
GRANT USAGE ON SCHEMA private TO trusted_role;

8. Table & View Privileges

PrivilegeWhat it allows
SELECTRead rows. Also required in practice for UPDATE and DELETE (to identify rows).
INSERTAdd new rows.
UPDATEModify existing rows.
DELETERemove rows.
TRUNCATEDelete all rows instantly without a WHERE clause.
REFERENCESCreate foreign key constraints that reference this table.
TRIGGERCreate triggers on this table.
ALL PRIVILEGESGrants all of the above in one statement.
-- Single table grants
GRANT SELECT ON orders TO alice;
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
GRANT ALL PRIVILEGES ON orders TO admin_user;

-- Grant on all currently existing tables in a schema
GRANT SELECT ON ALL TABLES IN SCHEMA public TO alice;

-- Revoke a specific privilege
REVOKE INSERT ON orders FROM alice;

-- Grant WITH GRANT OPTION: allows alice to grant this to others
GRANT SELECT ON orders TO alice WITH GRANT OPTION;
ℹ️ Views and Privileges
Views follow the same model as tables. A user only needs SELECT on the view itself — not on the underlying tables. The view executes with the owner's privileges (unless created with security_invoker = true). This makes views a powerful tool for controlled data exposure.

9. Column-Level Privileges

SELECT, INSERT, UPDATE, and REFERENCES can be granted at the column level for fine-grained access control.

-- Allow alice to only read name and email columns
GRANT SELECT (name, email) ON users TO alice;

-- Allow bob to update only the status column
GRANT UPDATE (status) ON orders TO bob;

-- Verify column-level grants
SELECT grantee, table_name, column_name, privilege_type
FROM information_schema.column_privileges
WHERE grantee = 'alice';
💡 Tip
Column-level grants are an alternative to creating views when you want to hide sensitive columns like password_hash, ssn, or credit_card_number from certain users without the overhead of maintaining separate view definitions.

10. Sequence Privileges

Sequences (used by SERIAL and IDENTITY columns) have their own privilege set. If an app user needs to INSERT into a table with an auto-increment ID, they also need sequence access.

PrivilegeWhat it allows
USAGECall nextval() and currval(). Required for INSERT on tables with serial/identity columns.
SELECTCall currval() only (read current value without advancing).
UPDATECall nextval(), setval() (advance or reset the sequence).
-- App user inserting rows needs USAGE on sequences
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app_user;

-- Read-only user typically only needs SELECT on sequences
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO readonly_user;

-- Grant on a specific sequence
GRANT USAGE ON SEQUENCE orders_id_seq TO app_user;

11. Function & Procedure Privileges

PrivilegeWhat it allowsDefault
EXECUTECall the function or stored procedure.Granted to PUBLIC
GRANT EXECUTE ON FUNCTION calculate_tax(numeric) TO app_user;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO app_user;

-- Lock down sensitive functions (override PUBLIC default)
REVOKE EXECUTE ON FUNCTION admin_reset_all_passwords() FROM PUBLIC;

Security Definer vs Security Invoker

Functions can run under two different privilege models:

ModeRuns with privileges ofUse case
SECURITY INVOKER DefaultThe callerMost functions — caller's privileges apply
SECURITY DEFINERThe function ownerControlled elevation — caller can do things the owner can, but only through this function
-- SECURITY DEFINER: function runs as owner, not caller
-- Useful for allowing users to do one specific elevated operation
CREATE OR REPLACE FUNCTION get_all_orders()
RETURNS SETOF orders
LANGUAGE sql
SECURITY DEFINER
SET search_path = public  -- always set this with SECURITY DEFINER!
AS $$
    SELECT * FROM orders;
$$;

12. Built-in Predefined Roles (PG 10+)

PostgreSQL ships with several predefined roles. These are especially convenient on managed databases (like Azure, RDS) where you don't have superuser access.

RoleWhat it grantsSince
pg_read_all_dataSELECT on all tables, views, sequences across all schemas. Automatically covers future objects.PG 14
pg_write_all_dataINSERT, UPDATE, DELETE on all tables and views across all schemas.PG 14
pg_monitorFull monitoring access: read stats, configuration, query text, etc. Includes the roles below.PG 10
pg_read_all_settingsRead all configuration parameters (including those normally restricted).PG 10
pg_read_all_statsRead all pg_stat_* views, including query text from pg_stat_activity.PG 10
pg_stat_scan_tablesExecute ANALYZE on all tables to update statistics.PG 10
pg_signal_backendSend signals to backends: cancel queries (pg_cancel_backend), terminate sessions (pg_terminate_backend).PG 9.6
pg_checkpointExecute CHECKPOINT command.PG 15
pg_read_server_filesRead files on the server filesystem (COPY FROM, pg_read_file()).PG 11
pg_write_server_filesWrite files on the server filesystem (COPY TO).PG 11
-- Grant full read-only access to all tables/views in all schemas
GRANT pg_read_all_data TO readonly_user;

-- Grant write access everywhere (use sparingly)
GRANT pg_write_all_data TO app_user;

-- Grant monitoring access (for tools like pgBadger, Datadog)
GRANT pg_monitor TO monitoring_user;
⚠️ Remember: INHERIT is required
These built-in roles only work if the user has INHERIT (which is the default). If the user was created with NOINHERIT, they must run SET ROLE pg_read_all_data before the privileges take effect.

13. Default Privileges

GRANT ON ALL TABLES IN SCHEMA only covers already-existing objects. Tables created in the future won't inherit those grants. ALTER DEFAULT PRIVILEGES fixes this.

-- Grant SELECT on all FUTURE tables that migration_user creates in public schema
ALTER DEFAULT PRIVILEGES
    FOR ROLE migration_user
    IN SCHEMA public
    GRANT SELECT ON TABLES TO readonly_user;

-- Grant INSERT/UPDATE/DELETE on all FUTURE tables
ALTER DEFAULT PRIVILEGES
    FOR ROLE migration_user
    IN SCHEMA public
    GRANT INSERT, UPDATE, DELETE ON TABLES TO app_user;

-- Grant USAGE on all FUTURE sequences (needed for INSERT with serial columns)
ALTER DEFAULT PRIVILEGES
    FOR ROLE migration_user
    IN SCHEMA public
    GRANT USAGE ON SEQUENCES TO app_user;

-- Grant EXECUTE on all FUTURE functions
ALTER DEFAULT PRIVILEGES
    FOR ROLE migration_user
    IN SCHEMA public
    GRANT EXECUTE ON FUNCTIONS TO app_user;

-- View current default privilege configuration
\ddp

-- Remove a default privilege setting
ALTER DEFAULT PRIVILEGES
    FOR ROLE migration_user
    IN SCHEMA public
    REVOKE SELECT ON TABLES FROM readonly_user;
ℹ️ FOR ROLE explained
FOR ROLE migration_user means: "apply these defaults when objects are created by migration_user". If you run migrations as the admin user, use FOR ROLE admin_user. If FOR ROLE is omitted, it defaults to the current executing user.

14. Row-Level Security (RLS)

RLS allows you to restrict which individual rows a user can see or modify — even when they have full SELECT on the table. Essential for multi-tenant applications.

-- Step 1: Enable RLS on the table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Step 2: Create a policy
-- USING = filter for SELECT/UPDATE/DELETE
-- WITH CHECK = filter for INSERT/UPDATE (new row must satisfy this)
CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::int);

-- Step 3: In your app, set the context before querying
SET app.tenant_id = '42';
SELECT * FROM orders; -- silently returns only tenant 42's rows

Separate read and write policies

-- Read policy: users see only their own rows
CREATE POLICY select_own_orders ON orders
    FOR SELECT
    USING (user_id = current_setting('app.current_user_id')::int);

-- Insert policy: users can only insert rows for themselves
CREATE POLICY insert_own_orders ON orders
    FOR INSERT
    WITH CHECK (user_id = current_setting('app.current_user_id')::int);

-- Admins bypass all RLS (policy that always returns true for admins)
CREATE POLICY admin_all ON orders
    USING (current_setting('app.role') = 'admin');

-- Drop a policy
DROP POLICY select_own_orders ON orders;

-- Disable RLS entirely
ALTER TABLE orders DISABLE ROW LEVEL SECURITY;
⚠️ Table Owners Bypass RLS by Default
The table owner and superusers bypass all RLS policies. To enforce RLS even on the owner, use:
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
Superusers always bypass RLS unless their role has NOBYPASSRLS.

15. Inspecting Privileges

psql meta-commands

CommandWhat it shows
\du or \du+All roles and their system attributes
\l or \l+All databases and their access privileges
\dn+All schemas and their access privileges
\dp tablenamePrivileges on a specific table/view/sequence
\dpAll table/view/sequence privileges in current schema
\ddpDefault privilege configuration
\df+ funcnameFunction definition including security mode and privileges

SQL queries

-- All table privileges for a specific user
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'alice';

-- All roles a user is a member of
SELECT r.rolname
FROM pg_roles r
JOIN pg_auth_members m ON r.oid = m.roleid
JOIN pg_roles u ON u.oid = m.member
WHERE u.rolname = 'alice';

-- All objects owned by a specific role
SELECT relname, relkind
FROM pg_class
WHERE relowner = 'alice'::regrole;

-- Programmatically check effective privileges
SELECT has_table_privilege('alice', 'orders', 'SELECT');    -- → true/false
SELECT has_schema_privilege('alice', 'public', 'USAGE');   -- → true/false
SELECT has_database_privilege('alice', 'mydb', 'CONNECT'); -- → true/false

-- View all RLS policies
SELECT schemaname, tablename, policyname, roles, cmd, qual
FROM pg_policies;

-- Find all non-default privileges (useful for auditing)
SELECT * FROM information_schema.role_table_grants
WHERE grantee != 'PUBLIC' AND grantee != table_schema;

16. Common Recipes

✅ Minimal Read-Only User (PG 14+)

CREATE USER readonly_user WITH LOGIN PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE mydb TO readonly_user;
GRANT pg_read_all_data TO readonly_user;
REVOKE CREATE ON SCHEMA public FROM PUBLIC; -- prevent table creation

✅ App User (CRUD, no DDL)

CREATE USER app_user WITH LOGIN PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app_user;
-- Cover future tables created by the migration user
ALTER DEFAULT PRIVILEGES FOR ROLE migration_user IN SCHEMA public
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES FOR ROLE migration_user IN SCHEMA public
    GRANT USAGE ON SEQUENCES TO app_user;

✅ Migration / Schema Owner User

CREATE USER migration_user WITH LOGIN PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE mydb TO migration_user;
GRANT CREATE ON DATABASE mydb TO migration_user;  -- create schemas
GRANT CREATE ON SCHEMA public TO migration_user;   -- create tables

✅ Group Role Pattern

-- Create a group role (no LOGIN)
CREATE ROLE readonly_group NOLOGIN;
GRANT USAGE ON SCHEMA public TO readonly_group;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_group;

-- Add users to the group
GRANT readonly_group TO alice, bob, charlie;
-- All three can now SELECT (assuming default INHERIT)

-- Remove a user from the group
REVOKE readonly_group FROM bob;

✅ Safely Remove a User

-- 1. Reassign all objects owned by the user
REASSIGN OWNED BY old_user TO admin_user;

-- 2. Drop all remaining privileges (grants, default privileges, etc.)
DROP OWNED BY old_user;

-- 3. Now it's safe to drop the user
DROP USER old_user;

17. Best Practices

#PracticeWhy
1Use group roles to assign privileges, not individual usersEasier to manage — add/remove users from a group vs. re-granting everything individually
2REVOKE CREATE ON SCHEMA public FROM PUBLICPrevents every user from creating tables in the public schema by default
3REVOKE CONNECT ON DATABASE mydb FROM PUBLICPrevents any new user from connecting without an explicit grant
4Always use ALTER DEFAULT PRIVILEGES for the migration userEnsures future tables/sequences are automatically accessible to app users
5Separate users by role: readonly, app, migrationPrinciple of least privilege — each user only has what it strictly needs
6Never use the superuser/admin account in your applicationLimits blast radius if credentials are compromised
7Prefer pg_read_all_data / pg_write_all_data on PG 14+Cleaner than manual grants; automatically covers all schemas including future ones
8Avoid NOINHERIT for regular app usersCauses confusing "permission denied" errors even after correctly granting roles
9Use REASSIGN OWNED + DROP OWNED before DROP USERAvoids the "role cannot be dropped because some objects depend on it" error
10Use VALID UNTIL and rotate passwords regularlyReduces exposure from leaked credentials over time
11Lock down SECURITY DEFINER functions by setting search_pathPrevents search path injection attacks against elevated-privilege functions
12Use RLS for multi-tenant data isolationData isolation at the database layer rather than relying solely on application logic
💡 Quick Sanity Check for any New User
Before deploying, verify each of these: