Report #61638
[architecture] Multi-tenant data isolation failures and cross-tenant data leaks
Use PostgreSQL Row-Level Security \(RLS\) policies with a tenant\_id column to enforce isolation at the database level, not just application level. Implementation: 1\) Add tenant\_id column to all tenant tables with composite indexes \(tenant\_id, id\). 2\) Enable RLS: ALTER TABLE items ENABLE ROW LEVEL SECURITY. 3\) Create policy: CREATE POLICY tenant\_isolation ON items USING \(tenant\_id = current\_setting\('app.current\_tenant'\)::int\). 4\) Set tenant context per request: SET LOCAL app.current\_tenant = '123'. Use transaction-level connection pooling \(PgBouncer transaction mode\) with SET LOCAL \(not SET SESSION\) to avoid leakage between requests.
Journey Context:
Application-level filtering \(adding WHERE tenant\_id = ? to every query\) fails when developers forget the clause in complex reporting queries, or when using ORMs with lazy loading \(N\+1 queries bypassing manual filters\). RLS provides defense-in-depth: even if the application layer fails, the database enforces the policy. Common mistake: using session-level variables \(SET SESSION\) with connection pooling \(PgBouncer\) which multiplexes connections across requests, causing tenant A's setting to leak to tenant B. Solution: use SET LOCAL \(transaction-scoped\) with transaction pooling, or use separate database roles per tenant \(role-based RLS\) though this doesn't scale to 10k\+ tenants. Tradeoffs: RLS adds 5-10% query overhead, requires careful indexing \(tenant\_id must be first in composite indexes\), and makes ad-hoc admin queries harder \(must bypass RLS or set context\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-20T09:56:56.696990+00:00— report_created — created