Guide
Multi-tenancy architecture explained
A project-management SaaS serves 12,000 companies from one deployment.
Acme Corp must never see Globex Industries' tasks, billing records, or
uploaded files — even if a developer forgets a WHERE clause.
Multi-tenancy is the architecture pattern where a single
application instance serves many logically separate customers (tenants)
while sharing compute, storage, and operational overhead. Done well, it
keeps unit economics healthy; done poorly, one tenant's traffic spike or
a missing filter leaks another customer's data. This guide covers
isolation models from shared-schema to database-per-tenant, how requests
resolve tenant context, security layers beyond application code,
customization without forking the codebase, and a checklist for shipping
B2B SaaS that scales without multiplying deployments.
Multi-tenant vs single-tenant vs hybrid
In a single-tenant deployment, each customer gets dedicated infrastructure — their own database, often their own Kubernetes namespace or VM. Isolation is physical: there is nothing to leak across because nothing is shared. The trade-off is cost and operational drag. Provisioning, patching, and monitoring 500 separate stacks does not scale for a $29/month product.
Multi-tenant systems amortize that overhead. One codebase, one deployment pipeline, one on-call rotation — but logical walls between tenants enforced in software. Most modern B2B SaaS starts multi-tenant and only offers single-tenant (sometimes called "dedicated" or "enterprise isolated") at premium price points for compliance or performance guarantees.
Hybrid models run most customers on shared pools while routing high-value or regulated tenants to dedicated shards or clusters. The application layer stays the same; routing and provisioning differ. This is common once you have both freemium users and Fortune 500 contracts with data-residency requirements.
Database isolation models
The hardest design choice is where tenant boundaries live in storage. Three patterns dominate, each with different cost, isolation strength, and migration complexity.
Shared database, shared schema
Every tenant's rows live in the same tables. A tenant_id
column (UUID, never sequential integer exposed in URLs) partitions data.
This is the cheapest to operate: one connection pool, one backup job,
one schema migration. It demands discipline — every query must filter
by tenant, including joins and subqueries. ORM middleware that injects
tenant scope helps, but it is not a substitute for code review and
integration tests that assert cross-tenant reads fail.
Shared database, separate schema
PostgreSQL schemas (or SQL Server schemas) give each tenant a namespace
within one database instance: tenant_acme.orders vs
tenant_globex.orders. Connection routing sets
search_path or equivalent after authentication. Isolation
is stronger — a forgotten filter in one schema cannot read another — but
migration scripts must run per schema, and thousands of schemas stress
catalog metadata and backup restore times.
Database per tenant
Each tenant gets a dedicated database (or dedicated cluster for the largest). This mirrors single-tenant isolation with shared application code. Provisioning automation becomes critical: creating databases, running migrations, rotating credentials, and monitoring per-tenant storage. Cross-tenant analytics require ETL into a warehouse. This pattern pairs naturally with database sharding when individual tenants outgrow a single node.
Choose shared schema until compliance or noisy-neighbor pain forces separation. Premature database-per-tenant multiplies ops work before product-market fit is proven.
Identifying the tenant on every request
Before any business logic runs, the system must resolve which tenant this request belongs to and reject ambiguous or forged context. Common resolution strategies:
- Subdomain —
acme.app.commaps to tenant Acme. Simple for users; requires wildcard TLS and careful cookie scoping. - Custom domain —
projects.acme.comCNAMEs to your platform. White-label friendly; DNS verification and certificate automation add complexity. - Path or header —
/t/acme/...orX-Tenant-ID. Works for APIs; easy to get wrong if clients can set headers arbitrarily without binding to auth. - JWT or session claim — tenant ID embedded in the token after login. Preferred for authenticated APIs: the tenant is cryptographically bound to the user, not chosen per request.
Anti-pattern: accepting tenant ID from an unauthenticated query parameter and trusting it. Always derive tenant context from authenticated identity or a verified domain mapping, then store it in request-scoped context (async local storage, request attributes) so downstream layers cannot accidentally operate without it.
Security beyond application filters
Application-level WHERE tenant_id = ? clauses are necessary
but not sufficient. Defense in depth for multi-tenant SaaS includes:
- Row-level security (RLS) — PostgreSQL policies that enforce tenant scope at the database layer even if application code regresses.
- Separate encryption keys per tenant — for regulated data, envelope encryption with a tenant-specific data key limits blast radius if a volume is copied.
- Authorization models — tenant admin vs member vs guest roles scoped inside the tenant boundary. See authentication vs authorization for separating identity from permission checks.
- Audit logs — append-only records of who accessed what, tagged with tenant ID, retained per contract.
- Penetration testing focused on IDOR — insecure direct object reference across tenants is the classic multi-tenant breach pattern.
Automated tests should include "tenant A token cannot read tenant B resource by ID" cases for every API surface — not just happy-path CRUD.
Noisy neighbors and fair sharing
Shared infrastructure means one tenant's batch import can slow everyone else's dashboards. Mitigations:
- Per-tenant rate limits at the API gateway — separate quotas from global platform limits.
- Resource pools — background job queues partitioned by tenant tier; enterprise tenants get dedicated worker concurrency.
- Query guardrails — statement timeouts, max rows returned, and rejection of unindexed full-table scans triggered by one tenant's report builder.
- Cache namespacing — Redis keys prefixed with tenant ID; never share cache entries across tenants.
- Observability per tenant — metrics and traces tagged with
tenant_id(hashed in exports if needed) so support can see who is consuming disproportionate CPU.
When a single tenant consistently dominates shared resources, that is the signal to migrate them to a dedicated shard or database — not to keep tuning shared pools indefinitely.
Customization without code forks
Enterprise buyers expect branding, feature toggles, and workflow differences. Multi-tenant SaaS handles this with configuration, not separate deployments:
- Feature flags evaluated per tenant — enable beta modules for design partners only.
- Theme and asset overrides stored in tenant settings (logo URL, primary color), applied at render time.
- Configurable fields and workflows — metadata schemas (custom attributes on entities) rather than hard-coded columns per customer.
- Webhooks and integrations — tenant-scoped OAuth apps and callback URLs so Acme's Slack bot never posts to Globex's channel.
The line between "configuration" and "tenant-specific code branch" should
be explicit. If more than a handful of tenants need bespoke logic, invest
in a plugin or rules engine — not if (tenantId === 'acme')
scattered through the codebase.
Onboarding, offboarding, and compliance
Provisioning should be idempotent: create tenant record, default roles, seed templates, optionally provision dedicated DB, emit welcome webhook. Failed halfway states must be resumable.
Offboarding is legally loaded: export customer data on request, then delete or anonymize within SLA. Soft-delete with a retention window beats immediate hard-delete when accidental cancellation is common.
Compliance drivers — SOC 2, HIPAA, GDPR, data residency
— often dictate isolation model. EU-only tenants may require databases in
eu-west-1 regardless of shared schema. Document which tenants
live in which region and enforce routing at the edge; do not rely on
developers remembering region flags in queries.
Multi-tenancy in microservices
In a microservices layout, tenant context must propagate on every internal call — gRPC metadata, trace baggage, or signed internal JWTs. A service that re-resolves tenant from a shared cache without validating the caller can become a lateral movement path. Prefer a gateway that authenticates once and forwards a verified tenant claim; internal services trust the mesh identity, not client-supplied headers from the public internet.
Event streams should include tenant ID in message keys or headers so consumers can partition work and enforce isolation in async paths the same way as synchronous HTTP.
Common anti-patterns
- Global admin APIs without tenant scope — "internal tools" that bypass filters cause the worst breaches.
- Sequential tenant IDs in URLs — enables enumeration; use opaque UUIDs.
- Shared S3 buckets without prefix policies — object keys must be tenant-scoped; pre-signed URLs must not be reusable across tenants.
- One-size backup without per-tenant restore — enterprise contracts require restoring a single tenant without rolling back everyone.
- Deferring isolation until "later" — retrofitting tenant_id onto a mature schema is slower and riskier than designing it in from day one.
Production checklist
- Document the chosen isolation model and the criteria for upgrading a tenant to dedicated resources.
- Resolve tenant context before authentication completes on every entry point (HTTP, WebSocket, queue consumer).
- Enforce tenant scope in the database (RLS or separate schemas) — not only in application code.
- Add cross-tenant negative tests to CI for every resource type.
- Namespace caches, queues, object storage keys, and search indexes by tenant.
- Apply per-tenant rate limits and query timeouts; alert on top-N resource consumers.
- Tag metrics, logs, and traces with tenant ID for support and capacity planning.
- Automate tenant provisioning and offboarding with idempotent workflows and audit trails.
- Support single-tenant data export and deletion without affecting other customers.
- Review feature-flag and customization paths so tenant logic does not fragment into unmaintainable branches.
Key takeaways
- Multi-tenancy trades isolation complexity for operational efficiency — choose how much isolation you sell at each price tier.
- Shared schema is the default until compliance, scale, or noisy-neighbor pain justifies separation.
- Tenant context must be cryptographically bound to authenticated identity, not client-supplied parameters.
- Database-level enforcement (RLS) catches bugs that code review misses.
- Fair sharing requires per-tenant limits and observability — otherwise one customer owns your p99 latency.
Related reading
- Microservices architecture explained — service boundaries and propagating context across internal calls
- Database sharding explained — when individual tenants outgrow a single database node
- Authentication vs authorization explained — identity, roles, and permission checks inside a tenant
- API rate limiting explained — per-tenant quotas and fair-use enforcement