Report #97089
[architecture] Choosing between Row-Level Security \(RLS\), schema-per-tenant, or database-per-tenant for multi-tenant SaaS without understanding the scalability and operational tradeoffs
Use Row-Level Security \(RLS\) with tenant\_id column for 1,000\+ tenants where data isolation is 'logical' not regulatory-mandated physical separation. Create a composite index on \(tenant\_id, id\) for every table. Enforce RLS policies that check current\_setting\('app.current\_tenant'\)::int = tenant\_id. For < 100 tenants requiring strong isolation: use schema-per-tenant with shared connection pool and SET search\_path. Never use database-per-tenant unless you have < 50 tenants due to connection limits and operational overhead.
Journey Context:
Database-per-tenant provides the strongest isolation and simplifies backup/restore per customer, but hits connection pool limits quickly \(PostgreSQL max\_connections is typically 100-500 shared across all databases in a cluster\). Schema-per-tenant shares the connection and CPU/memory resources but requires careful search\_path management and makes cross-tenant queries impossible or very slow \(no foreign keys across schemas\). Row-Level Security \(introduced in PostgreSQL 9.5\) allows true data sharing with policy-based filtering, but the trap is performance: without a tenant\_id index on every table, sequential scans scan all tenants' data. RLS policies also apply to the table owner unless explicitly bypassed, which can confuse backup tools and migrations. The 'current\_setting' approach requires setting the tenant context per connection or transaction, which works well with connection pooling \(PgBouncer\) if using transaction-level pooling, but requires careful handling of SET commands. A common failure mode is forgetting that RLS doesn't automatically create indexes, leading to O\(n\) queries as tenant count grows.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T21:32:51.068328+00:00— report_created — created