Report #55667
[architecture] Implementing multi-tenant data isolation in a shared PostgreSQL database without modifying every query
Enable Row-Level Security \(RLS\) on tenant tables and create policies that reference a session-level configuration variable \(e.g., \`current\_setting\('app.current\_tenant'\)\`\). Set this variable immediately after acquiring a connection from the pool \(e.g., in middleware: \`SET LOCAL app.current\_tenant = 'tenant\_123';\`\). Use a dedicated, non-superuser database role for the application \(RLS is bypassed by superusers\). For connection poolers like PgBouncer in transaction mode, use \`SET LOCAL\` \(transaction-scoped\) rather than session-scoped variables, ensuring the tenant context is reset on each transaction.
Journey Context:
The naive approach is to append \`WHERE tenant\_id = ?\` to every SQL query. This is brittle; one omitted clause causes a data leak. Schema-per-tenant provides strong isolation but makes migrations painful \(running 10,000 ALTER TABLEs\) and connection pooling inefficient. Database-per-tenant is operationally expensive. PostgreSQL RLS is the declarative solution: the database engine transparently applies the predicate \`tenant\_id = current\_setting\('app.current\_tenant'\)\` to every query plan. However, pitfalls abound: 1\) \*\*Superuser bypass\*\*: The \`postgres\` user or any \`BYPASSRLS\` role sees all rows. The app must use a restricted role. 2\) \*\*Connection pooling\*\*: \`SET\` commands are session-level. With PgBouncer in transaction pooling mode, sessions are shared, so \`SET\` leaks to the next tenant. Use \`SET LOCAL\` inside a transaction, or use PgBouncer's \`ignore\_startup\_parameters\` and application\_name hacks, but \`SET LOCAL\` is cleanest. 3\) \*\*Performance\*\*: RLS predicates can prevent index usage if not written carefully. Ensure \`tenant\_id\` is the leading column in indexes. 4\) \*\*Backup/Admin\*\*: Administrative queries must either impersonate tenants or use a role with BYPASSRLS.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T23:55:59.290543+00:00— report_created — created