Multi-Tenant SaaS Architecture: Patterns We Use in Production
Multi-tenant architecture is the single hardest decision to change after a SaaS product launches. Get it wrong and you're not refactoring a feature — you're migrating live customer data under a data model that was never designed to support it. Here are the patterns we actually use in production, and how we decide between them.
What "multi-tenant" actually means
Multiple customer organisations (tenants) share the same application and infrastructure, while their data stays fully isolated from each other. The isolation guarantee is the entire point — a bug that lets Tenant A see Tenant B's data isn't a minor issue, it's the kind of incident that ends customer trust and, depending on the industry, triggers real compliance consequences.
There are three broad patterns, each with real tradeoffs.
Pattern 1: Shared database, shared schema (row-level isolation)
Every tenant's data lives in the same tables, isolated by a tenant_id (or organisation_id) column and enforced access rules.
-- Every tenant-scoped table carries this column
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organisation_id UUID NOT NULL REFERENCES organisations(id),
amount NUMERIC NOT NULL,
status TEXT NOT NULL
);
-- Postgres Row-Level Security enforces isolation at the database layer,
-- not just in application code that could forget a WHERE clause
CREATE POLICY tenant_isolation ON invoices
USING (organisation_id = current_setting('app.current_tenant')::uuid);
Why we use this as the default. It scales efficiently to a large number of small-to-medium tenants, keeps infrastructure and operational overhead low (one database to back up, monitor, and migrate), and — critically — Postgres Row-Level Security enforces isolation at the database layer. That's the detail that matters: isolation shouldn't depend on every engineer remembering to add a WHERE organisation_id = ? to every query. RLS makes a missed filter fail closed instead of leaking data.
The real risk. If you rely on application-layer filtering alone (no RLS, just remembering to scope every query), one missed filter in one endpoint is a data leak. This is the single most common multi-tenant security mistake we see in codebases we're brought in to review.
Pattern 2: Shared database, schema-per-tenant
Each tenant gets its own Postgres schema within the same database — same physical infrastructure, logically separated namespaces.
When it fits. A middle ground when you want stronger logical separation than row-level isolation but don't want the operational overhead of fully separate databases. Useful when tenants occasionally need genuinely different table structures (rare, but it happens with enterprise customisation).
The tradeoff. Migrations become more complex — a schema change now needs to run against every tenant's schema, not one shared table. This scales poorly past a few hundred tenants unless you invest in solid migration tooling.
Pattern 3: Database-per-tenant
Each tenant gets a fully separate database.
When it fits. Strong isolation requirements — data residency regulations, contractual guarantees, or enterprise customers who specifically require dedicated infrastructure. Also useful when a small number of large tenants have very different scale profiles from each other (one tenant's load shouldn't affect another's performance).
The tradeoff. Operational overhead scales linearly with tenant count. Connection pooling, migrations, monitoring, and backups all multiply. This pattern makes sense for tens or low hundreds of large tenants, not thousands of small ones.
How we actually decide
For the SaaS development work we do, we default to shared schema with enforced row-level security unless there's a specific, articulated reason not to — data residency requirements, a contractual isolation guarantee from a specific large customer, or genuinely divergent per-tenant data models. Defaulting to database-per-tenant "for safety" without a real requirement just adds operational cost without a corresponding benefit for most SaaS products.
Beyond the database: what else needs tenant-awareness
Getting the database pattern right isn't the whole job. Every layer of the system needs to respect tenant boundaries:
- Background jobs and queues need the tenant context passed through explicitly — a job processing an async task after the original request context is gone is a common place tenant scoping gets accidentally dropped.
- File storage (S3 or equivalent) needs tenant-scoped paths or buckets, with access policies that mirror the database isolation guarantee.
- Caching layers (Redis, in-memory caches) need tenant-scoped keys — a shared cache key across tenants is a subtle, easy-to-miss data leak vector.
- Search indexes (if you're using Elasticsearch or similar) need the same row-level filtering discipline as the primary database.
Getting the foundation right the first time
Multi-tenancy is the kind of decision that's cheap to get right at the start and expensive to fix later — not because the concept is hard, but because changing it means migrating live customer data under a model that was never designed for it. If you're scoping a new SaaS product and want the tenant isolation strategy designed properly from day one, talk to us about what your specific isolation and scale requirements actually are.
Talk to us about saas development
Tell us what you're building and we'll give you a clear, honest assessment.
