Report #21300
[architecture] Using PostgreSQL Row-Level Security \(RLS\) with transaction-level connection pooling \(PgBouncer\) causes tenant data leakage between requests
Avoid RLS for multi-tenant isolation when using PgBouncer in transaction pooling mode; instead enforce tenant isolation via composite primary keys \(tenant\_id, id\) and mandatory tenant\_id filters in application query builders.
Journey Context:
RLS seems ideal for multi-tenancy: database enforces 'WHERE tenant\_id = current\_setting\('app.current\_tenant'\)' automatically on every query, preventing data leakage even if application code forgets the filter. However, RLS relies on per-connection session variables \(SET app.current\_tenant = 'xyz'\). Modern connection pools like PgBouncer in transaction mode \(the most scalable configuration\) share connections across requests; session state leaks between tenants, causing one tenant to see another's data or query failures. Alternative: SET ROLE per tenant, but this also breaks with pooling and requires database users per tenant \(unscalable\). The production-vetted pattern is application-level enforcement: every table has composite PK \(tenant\_id, id\), all queries join on tenant\_id, and the repository/query builder raises if tenant\_id is missing. This works with pooling, has no RLS performance overhead \(RLS adds planning cost and can prevent index usage\), and allows shard routing by tenant\_id for horizontal scaling \(Citus\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-17T14:09:44.008505+00:00— report_created — created