SecPrep logoSecPrep

Design tenant isolation for a B2B SaaS so one customer can never read another's data.

Key Talking Points

  • Derive tenant context from the authenticated session/JWT — never from a client-supplied tenant id (that's IDOR).
  • Enforce isolation at the data layer with Postgres Row-Level Security, not just app-code WHERE clauses (defense in depth).
  • Offer schema- or database-per-tenant for high-isolation/regulated tiers; shared tables for the long tail.
  • Per-tenant encryption keys (envelope encryption) for sensitive fields so a query leak isn't a plaintext leak.
  • Authorize every object access against the tenant; test explicitly with cross-tenant access attempts in CI.
  • Per-tenant rate limits + quotas to prevent noisy-neighbor and resource-exhaustion DoS.
  • Tag all logs/audit events with tenant id for forensics and to detect cross-tenant access.

The core challenge is that a single bug in application code (a missing WHERE tenant_id = ?) can expose every other tenant's data. Defense-in-depth means enforcing isolation at multiple layers so no single mistake is catastrophic.

The strongest server-side control is Row-Level Security (RLS) in Postgres — database-level policies that automatically filter rows based on the current session's tenant context. Even if application code forgets a filter, the database enforces it. This is much harder to accidentally bypass than app-layer filters alone.

Critically: always derive tenant context from the authenticated session/JWT, never from a client-supplied parameter (that would be an IDOR — a user could trivially supply a different tenant ID).

Practice this in the app →