PostgreSQL
RLS Multi-tenancy Pattern
Non-negotiables
- RLS context is mandatory for any tenant-scoped query
- Context must be set inside the same transaction as the queries
- No fallbacks for tenant ID (fail fast if missing)
- Async-only DB access when using async frameworks
Setting RLS Context
RLS works only if the current transaction has the context set:
SET LOCAL app.current_tenant_id = '<tenant_uuid>';
Must run before the first tenant-scoped query in that transaction.
Common Failure Modes
- Setting
SET LOCAL ... after the first select()
- Setting the context in one session, then querying in another
- Running queries outside the expected transaction scope
Typical RLS Policy
ALTER TABLE some_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY some_table_tenant_isolation
ON some_table
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
Multi-tenant Table Checklist
- Tenant ID column is UUID
- FK to tenants table with
ON DELETE CASCADE
- Indexes aligned with access patterns (usually tenant_id first)
- PostgreSQL does not auto-index FK columns — add explicit indexes
- UNIQUE allows multiple NULLs unless using
NULLS NOT DISTINCT (PG15+)
- RLS is enabled and policies exist
- Application code sets RLS context at transaction start
Alembic Migrations Checklist
- Add/modify schema (columns, constraints, FKs)
- Create/update indexes
- Enable RLS and create/adjust policies
- Add verification (tests) for isolation
- Provide a real downgrade (no stubs)
RLS Isolation Testing Recipe
Goal:
- Data for tenant A is visible to tenant A
- Data for tenant A is NOT visible to tenant B
Canonical flow:
- Setup data through an admin session (RLS bypass) for tenant A and B
- Assert via an RLS session:
- set context to tenant A → sees only tenant A data
- set context to tenant B → does not see tenant A data
Destructive Operations Safety
Hard rules:
- Never run
DELETE without a narrow WHERE targeting specific data
- Never run
TRUNCATE/DROP without explicit confirmation
Pre-flight before destructive actions:
- Confirm exact target (tables / IDs / date range)
- Run a
SELECT/row count first and show results
- Ask for final confirmation, then execute
References
Schema & Design
- table-design.md — Data types, constraints, indexing, partitioning, JSONB, safe schema evolution
- charset-encoding.md — Character sets, encoding, collation, ICU, locale settings
Authentication
- authentication.md — pg_hba.conf, SCRAM-SHA-256, md5, peer, cert, LDAP, GSSAPI
- authentication-oauth.md — OAuth 2.0 (PostgreSQL 18+), SASL OAUTHBEARER, validators
- user-management.md — CREATE/ALTER/DROP ROLE, membership, GRANT/REVOKE, predefined roles
Runtime Configuration
- connection-settings.md — listen_addresses, max_connections, SSL, TCP keepalives
- query-tuning.md — Planner settings, work_mem, parallel query, cost constants
- replication.md — Streaming replication, WAL, synchronous commit, logical replication
- vacuum.md — Autovacuum, vacuum cost model, freeze ages, per-table tuning
- error-handling.md — exit_on_error, restart_after_crash, data_sync_retry
Internals
- internals.md — Query processing pipeline, parser/rewriter/planner/executor, system catalogs, wire protocol, access methods
- protocol.md — Wire protocol v3.2: message format, startup, auth, query, COPY, replication
Links
See Also
- sql-expert — Query patterns, EXPLAIN workflow, optimization
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: itechmeat-llm-code-postgresql3description: PostgreSQL4---56# PostgreSQL78## RLS Multi-tenancy Pattern910### Non-negotiables1112- **RLS context is mandatory** for any tenant-scoped query13- **Context must be set inside the same transaction** as the queries14- **No fallbacks** for tenant ID (fail fast if missing)15- **Async-only** DB access when using async frameworks1617### Setting RLS Context1819RLS works only if the current transaction has the context set:2021```sql22SET LOCAL app.current_tenant_id = '<tenant_uuid>';23```2425Must run before the first tenant-scoped query in that transaction.2627### Common Failure Modes2829- Setting `SET LOCAL ...` after the first `select()`30- Setting the context in one session, then querying in another31- Running queries outside the expected transaction scope3233### Typical RLS Policy3435```sql36ALTER TABLE some_table ENABLE ROW LEVEL SECURITY;3738CREATE POLICY some_table_tenant_isolation39ON some_table40USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);41```4243## Multi-tenant Table Checklist4445- Tenant ID column is **UUID**46- FK to tenants table with `ON DELETE CASCADE`47- Indexes aligned with access patterns (usually tenant_id first)48 - PostgreSQL does **not** auto-index FK columns — add explicit indexes49 - UNIQUE allows multiple NULLs unless using `NULLS NOT DISTINCT` (PG15+)50- RLS is enabled and policies exist51- Application code sets RLS context at transaction start5253## Alembic Migrations Checklist54551. Add/modify schema (columns, constraints, FKs)562. Create/update indexes573. Enable RLS and create/adjust policies584. Add verification (tests) for isolation595. Provide a real downgrade (no stubs)6061## RLS Isolation Testing Recipe6263Goal:6465- Data for tenant A is visible to tenant A66- Data for tenant A is NOT visible to tenant B6768Canonical flow:69701. Setup data through an **admin session** (RLS bypass) for tenant A and B712. Assert via an **RLS session**:72 - set context to tenant A → sees only tenant A data73 - set context to tenant B → does not see tenant A data7475## Destructive Operations Safety7677Hard rules:7879- Never run `DELETE` without a narrow `WHERE` targeting specific data80- Never run `TRUNCATE`/`DROP` without explicit confirmation8182Pre-flight before destructive actions:83841. Confirm exact target (tables / IDs / date range)852. Run a `SELECT`/row count first and show results863. Ask for final confirmation, then execute8788## References8990### Schema & Design9192- [table-design.md](references/table-design.md) — Data types, constraints, indexing, partitioning, JSONB, safe schema evolution93- [charset-encoding.md](references/charset-encoding.md) — Character sets, encoding, collation, ICU, locale settings9495### Authentication9697- [authentication.md](references/authentication.md) — pg_hba.conf, SCRAM-SHA-256, md5, peer, cert, LDAP, GSSAPI98- [authentication-oauth.md](references/authentication-oauth.md) — OAuth 2.0 (PostgreSQL 18+), SASL OAUTHBEARER, validators99- [user-management.md](references/user-management.md) — CREATE/ALTER/DROP ROLE, membership, GRANT/REVOKE, predefined roles100101### Runtime Configuration102103- [connection-settings.md](references/connection-settings.md) — listen_addresses, max_connections, SSL, TCP keepalives104- [query-tuning.md](references/query-tuning.md) — Planner settings, work_mem, parallel query, cost constants105- [replication.md](references/replication.md) — Streaming replication, WAL, synchronous commit, logical replication106- [vacuum.md](references/vacuum.md) — Autovacuum, vacuum cost model, freeze ages, per-table tuning107- [error-handling.md](references/error-handling.md) — exit_on_error, restart_after_crash, data_sync_retry108109### Internals110111- [internals.md](references/internals.md) — Query processing pipeline, parser/rewriter/planner/executor, system catalogs, wire protocol, access methods112- [protocol.md](references/protocol.md) — Wire protocol v3.2: message format, startup, auth, query, COPY, replication113114## Links115116- [Documentation](https://www.postgresql.org/docs/current/)117- [Releases](https://www.postgresql.org/about/newsarchive/pgsql/)118- [GitHub](https://github.com/postgres/postgres)119120## See Also121122- [sql-expert](../sql-expert/SKILL.md) — Query patterns, EXPLAIN workflow, optimization123124---125> Converted and distributed by [TomeVault](https://tomevault.io/claim/itechmeat) — claim your Tome and manage your conversions.126<!-- tomevault:4.0:skill_md:2026-04-11 -->