All posts
Architecture

Designing Multi-Tenant SaaS Architecture: Data Isolation and Picking the Right Model

Three tenant-isolation patterns — shared DB with RLS, schema-per-tenant, and DB-per-tenant — when to pick each, with a Postgres RLS example.

PB
PhiBit
PhiBit · phi-bit.com

Every SaaS product serves multiple customers (tenants) from shared infrastructure, and the most consequential early architectural decision is: how do we isolate each tenant's data from the others? This choice shapes security, cost, compliance, and scalability for years, and reversing it later is painful. This article walks through the three common patterns and when each fits.

The Three Tenant-Isolation Patterns

Most SaaS designs sit on a spectrum between full logical isolation (share everything) and full physical isolation (separate everything):

  • Shared database + Row-Level Security (RLS): all tenants live in the same tables, distinguished by a tenant_id column, with isolation enforced at the row level inside the database itself. Cheapest and simplest to operate; best for early stage and high-scale SaaS with many tenants.
  • Schema-per-tenant: one database partitioned into a separate schema per tenant. Clearer logical isolation and easier selective backups, but migrations get heavy once you pass a few hundred tenants.
  • Database-per-tenant: full physical isolation — the most secure and the best fit for data-residency requirements and enterprise customers, but the most expensive to operate and the hardest to scale horizontally.

The Core Problems: Noisy Neighbors and Data Residency

In the shared pattern, the noisy-neighbor problem appears: one heavy tenant consumes database resources and everyone else degrades. Mitigate it with resource quotas, solid indexing on tenant_id, and sometimes peeling large tenants off into their own databases. Data residency, meanwhile, is a legal requirement in some countries and sectors that a customer's data stay within a defined geographic boundary — there, splitting databases by region (or a schema/DB per region) becomes a necessity rather than a choice.

Example: Enforcing RLS in Postgres

In the shared pattern, the most common security mistake is relying on a WHERE tenant_id clause in application code alone — any query that forgets the clause leaks one tenant's data to another. The correct approach enforces isolation inside the database via Row-Level Security, so it cannot be bypassed from the application layer:

-- Enable row-level security on the tenant-scoped table
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;

-- Policy: a row is only visible when its tenant_id
-- matches the tenant set for the current session
CREATE POLICY tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- The application sets this per request, inside the
-- transaction, after authenticating the tenant:
--   SET LOCAL app.tenant_id = '...uuid...';
-- Now every query is automatically scoped to that tenant,
-- even if the WHERE clause forgets it.

How to Choose

  1. Start with shared DB + RLS unless you have a strong reason not to — it is the lowest cost and the fastest path to market.
  2. Move to schema- or database-per-tenant when an enterprise or regulated customer mandates physical isolation or explicit data residency.
  3. Adopt a pragmatic hybrid: the majority on a shared database, with large or regulated customers on dedicated databases. This is what we prefer at PhiBit because it balances cost against enterprise requirements.

Building a SaaS product and unsure which isolation model fits your case? Email us at [email protected] and we'll gladly talk through the options based on your regulatory needs and expected scale.

Ready to turn your idea into a product?

Get a detailed quote within 24 hours. First consultation is free, no strings attached.

PhiBit Ltd