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:
- Log in to the database (if they have
LOGIN) - Own database objects (tables, schemas, functions, etc.)
- Be granted privileges on objects
- Be members of other roles (acting as groups)
- Inherit privileges from roles they belong to (if
INHERIT)
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.
| Attribute | Default | What it controls |
|---|---|---|
LOGIN / NOLOGIN | NOLOGIN | Can the role connect to the database server? |
SUPERUSER / NOSUPERUSER | NOSUPERUSER | Bypasses ALL access checks. Extremely dangerous to hand out. |
CREATEDB / NOCREATEDB | NOCREATEDB | Can the role create new databases? |
CREATEROLE / NOCREATEROLE | NOCREATEROLE | Can the role create, alter, or drop other roles? |
INHERIT / NOINHERIT | INHERIT | Does the role automatically inherit privileges from roles it belongs to? |
REPLICATION / NOREPLICATION | NOREPLICATION | Can the role initiate streaming replication? |
BYPASSRLS / NOBYPASSRLS | NOBYPASSRLS | Can the role bypass Row-Level Security policies? |
CONNECTION LIMIT n | -1 (unlimited) | Maximum concurrent connections for this role. |
PASSWORD | null | Login password (stored hashed). |
VALID UNTIL | never expires | Password expiry timestamp. |
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.
CONNECTon all databases → every user can connect to every databaseTEMPon all databases → every user can create temporary tablesCREATEon thepublicschema → every user can create tablesUSAGEon thepublicschema → every user can see objects in itEXECUTEon all functions → every user can call all functions
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.
| Setting | Behavior |
|---|---|
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;
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.
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.
| Privilege | What it allows | Default |
|---|---|---|
CONNECT | Connect to the database. | Granted to PUBLIC |
CREATE | Create new schemas within the database. | Not granted |
TEMP / TEMPORARY | Create 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.
| Privilege | What it allows | Default |
|---|---|---|
USAGE | Look up and access objects in the schema. Required to query any table in the schema. | Granted to PUBLIC on public |
CREATE | Create 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
| Privilege | What it allows |
|---|---|
SELECT | Read rows. Also required in practice for UPDATE and DELETE (to identify rows). |
INSERT | Add new rows. |
UPDATE | Modify existing rows. |
DELETE | Remove rows. |
TRUNCATE | Delete all rows instantly without a WHERE clause. |
REFERENCES | Create foreign key constraints that reference this table. |
TRIGGER | Create triggers on this table. |
ALL PRIVILEGES | Grants 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;
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';
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.
| Privilege | What it allows |
|---|---|
USAGE | Call nextval() and currval(). Required for INSERT on tables with serial/identity columns. |
SELECT | Call currval() only (read current value without advancing). |
UPDATE | Call 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
| Privilege | What it allows | Default |
|---|---|---|
EXECUTE | Call 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:
| Mode | Runs with privileges of | Use case |
|---|---|---|
SECURITY INVOKER Default | The caller | Most functions — caller's privileges apply |
SECURITY DEFINER | The function owner | Controlled 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.
| Role | What it grants | Since |
|---|---|---|
pg_read_all_data | SELECT on all tables, views, sequences across all schemas. Automatically covers future objects. | PG 14 |
pg_write_all_data | INSERT, UPDATE, DELETE on all tables and views across all schemas. | PG 14 |
pg_monitor | Full monitoring access: read stats, configuration, query text, etc. Includes the roles below. | PG 10 |
pg_read_all_settings | Read all configuration parameters (including those normally restricted). | PG 10 |
pg_read_all_stats | Read all pg_stat_* views, including query text from pg_stat_activity. | PG 10 |
pg_stat_scan_tables | Execute ANALYZE on all tables to update statistics. | PG 10 |
pg_signal_backend | Send signals to backends: cancel queries (pg_cancel_backend), terminate sessions (pg_terminate_backend). | PG 9.6 |
pg_checkpoint | Execute CHECKPOINT command. | PG 15 |
pg_read_server_files | Read files on the server filesystem (COPY FROM, pg_read_file()). | PG 11 |
pg_write_server_files | Write 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;
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 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;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;Superusers always bypass RLS unless their role has
NOBYPASSRLS.
15. Inspecting Privileges
psql meta-commands
| Command | What 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 tablename | Privileges on a specific table/view/sequence |
\dp | All table/view/sequence privileges in current schema |
\ddp | Default privilege configuration |
\df+ funcname | Function 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
| # | Practice | Why |
|---|---|---|
| 1 | Use group roles to assign privileges, not individual users | Easier to manage — add/remove users from a group vs. re-granting everything individually |
| 2 | REVOKE CREATE ON SCHEMA public FROM PUBLIC | Prevents every user from creating tables in the public schema by default |
| 3 | REVOKE CONNECT ON DATABASE mydb FROM PUBLIC | Prevents any new user from connecting without an explicit grant |
| 4 | Always use ALTER DEFAULT PRIVILEGES for the migration user | Ensures future tables/sequences are automatically accessible to app users |
| 5 | Separate users by role: readonly, app, migration | Principle of least privilege — each user only has what it strictly needs |
| 6 | Never use the superuser/admin account in your application | Limits blast radius if credentials are compromised |
| 7 | Prefer pg_read_all_data / pg_write_all_data on PG 14+ | Cleaner than manual grants; automatically covers all schemas including future ones |
| 8 | Avoid NOINHERIT for regular app users | Causes confusing "permission denied" errors even after correctly granting roles |
| 9 | Use REASSIGN OWNED + DROP OWNED before DROP USER | Avoids the "role cannot be dropped because some objects depend on it" error |
| 10 | Use VALID UNTIL and rotate passwords regularly | Reduces exposure from leaked credentials over time |
| 11 | Lock down SECURITY DEFINER functions by setting search_path | Prevents search path injection attacks against elevated-privilege functions |
| 12 | Use RLS for multi-tenant data isolation | Data isolation at the database layer rather than relying solely on application logic |
- Can the user connect? →
has_database_privilege('user', 'db', 'CONNECT') - Can the user see the schema? →
has_schema_privilege('user', 'public', 'USAGE') - Can the user read tables? →
has_table_privilege('user', 'tablename', 'SELECT') - Can the user create tables? →
has_schema_privilege('user', 'public', 'CREATE')(should befalsefor readonly/app users)