Report #15557
[architecture] Multi-tenant data isolation fails with manual WHERE clauses or suffers connection pool exhaustion with DB-per-tenant
Use PostgreSQL Row-Level Security \(RLS\) with tenant-specific session variables and enforce index strategies: SET app.current\_tenant = 'uuid'; with policies USING \(tenant\_id = current\_setting\('app.current\_tenant'\)::UUID\). Create all indexes with tenant\_id as the leftmost column \(tenant\_id, other\_cols\) to enable efficient index scans per tenant. For connection poolers \(PgBouncer\), use transaction-level SET LOCAL rather than session-level variables.
Journey Context:
Manual filtering \(WHERE tenant\_id = ?\) is error-prone; one missed clause causes catastrophic data leakage. Schema-per-tenant \(SET search\_path\) or DB-per-tenant provide strong isolation but break standard connection pooling \(PgBouncer/RDS Proxy\) because each tenant requires a distinct connection or schema switch, exhausting limited pools at scale \(thousands of tenants\). RLS provides defense-in-depth: the database enforces the predicate transparently, even for complex queries with JOINs. However, naive RLS implementation fails without proper indexing: if the index is on \(user\_id\) and the RLS policy filters tenant\_id, the database must scan all rows for a tenant. The index must lead with tenant\_id to make the RLS predicate an index condition. Connection pooling requires careful handling: RLS policies using current\_setting rely on session state, which persists across pooled connections. Using SET LOCAL \(transaction-scoped\) or aggressively resetting variables on connection return is mandatory. Tradeoff: RLS adds ~5-10% query overhead and complicates superuser queries \(bypass RLS must be explicitly enabled\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-17T00:24:20.057395+00:00— report_created — created