A single-user app has a comforting property: when your authorization check fails, one person’s data leaks. A SaaS does not have that property. One forgotten WHERE clause in a multi-tenant system and you are explaining to three hundred customers why customer 301 could read their invoices. Multi-tenancy is what makes SaaS economics work — the thousandth customer costs a fraction of the tenth — and it is also what makes SaaS security different: the blast radius of every bug is multiplied by every tenant you have.
That is why this series starts here, before SSO, before compliance, before the pentest. Tenant isolation is the foundation every other SaaS security control assumes. This series has no fixed table of contents — it is written one part at a time, each part picking the next most urgent door. Part one is the door everything else leans on.
The mindset: trust is the product
A SaaS does not sell features; it sells the belief that a customer’s data is safer with you than with them. Every control in this series traces back to that belief. And the first thing the belief requires is a crisp answer, on every request, to one question: whose data is this? The answer is the tenant context — and the discipline is that it can only ever come from a verified identity claim, never from anything the client sent you.
The three isolation models (and where each breaks)
Tenant data can be separated three ways, trading cost against isolation strength:
- Shared schema + RLS — every tenant’s rows in the same tables, marked by a
tenant_idcolumn, with Postgres row-level security enforcing the boundary. Cheapest to run (one pool, one migration path, one backup) and the right default for most SaaS. The risk concentrates exactly where you’d guess: isolation lives or dies with the policy. - Schema per tenant — one database, one Postgres schema per customer. Stronger logical separation and clean per-tenant backups, but migrations run N times and connection management gets awkward past a few hundred tenants. Fits mid-market contractual requirements; reportedly 30–50% cheaper to run than full database-per-tenant.
- Database per tenant — physical isolation. Simplest to reason about, easiest to sell into regulated buyers (HIPAA, PCI, government), and the most expensive: backups, monitoring, patching, and migrations all scale per customer. Reserve it for contracts that pay for it.
The 2026 landing spot for most successful SaaS is hybrid: SMB tenants pooled, enterprise tenants carved out to dedicated infrastructure — usually triggered by the first contract large enough to demand a signed DPA and an infra diagram. Plan that carve-out path in week one, not in the quarter the deal arrives; budget roughly 3–6 engineer-weeks per enterprise migration.
The four rules that stop leaks
Every cross-tenant incident investigation ends up at the same missing controls. Ship all four before the first paying customer:
tenant_idon every table, no exceptions — including audit logs, background job payloads, analytics tables, and “temporary” tables. Enforce it with a migration hook or linter, not with memory.- Row-level security in Postgres —
ENABLE ROW LEVEL SECURITYplus a policy per table, andSET LOCAL app.tenant_idat the start of every transaction. The database becomes the enforcement point: the query someone writes at 11 p.m. cannot leak, because the policy — not the developer — filters the rows. Default is deny. This is why RLS quietly became the 2026 default for new multi-tenant work: auditors increasingly expect it, the planner is mature, and the pooling story is solved. - Tenant claims only from verified tokens — extract
tenant_idfrom the signed JWT you just verified, and re-check it on every request. The request body, the URL, and the subdomain are attacker-controlled input, not identity. - Cross-tenant tests in CI — a test suite that authenticates as tenant A and attempts to read tenant B through every endpoint, run on every PR. This is the control that turns rules 1–3 from intentions into evidence.
The footguns that bypass all of it
Four recurring ways teams accidentally switch their own isolation off:
- The service-role key in user paths — in Supabase-style stacks, the service key bypasses RLS entirely. One server route that initializes the client with the service key and your policies are decorative. Rule: user requests always use a tenant-scoped, RLS-respecting connection; service keys only in isolated admin jobs.
tenant_idfrom the request — accepting the tenant from a body field, query param, or subdomain and using it to scope queries. Attackers enumerate subdomains and probe exactly this.- Subdomain as isolation —
acme.yourapp.comis a routing convenience, not a security boundary. Authentication and authorization still happen on the claim, or they don’t happen at all. - One shared KMS key for tenant secrets — a single leaked key decrypts every tenant’s data. Per-tenant keys (or per-tenant data-key wrapping) turn a key compromise into a single-tenant incident.
Where the series goes next
Likely next door: identity — the day your first enterprise prospect replies “do you support SSO?” and you discover that SAML, SCIM provisioning, and “Sign in with Google” are three very different things, two of which enterprise buyers actually mean. But the order is allowed to change — the series picks the next door when we get there, based on what’s most urgent.
Practice
- Pick one table in your current project and write the RLS policy for it by hand:
ENABLE ROW LEVEL SECURITY,CREATE POLICY ... USING (tenant_id = current_setting('app.tenant_id')), thenSET LOCALin a session and watch wrong-tenant queries return empty. - Write one cross-tenant test: create fixtures for tenants A and B, authenticate as A, assert 404/empty for B’s resources on three endpoints. Put it in CI.
- Grep your codebase for where
tenant_id(ororg_id,account_id) enters a query. Trace each one back to a verified claim. Anything that traces back to a request parameter is a bug you get to fix before someone else finds it.