name: Multi-Tenancy & SaaS Architecture
description: Patterns for building B2B SaaS multi-tenancy: tenant identification, data isolation (RLS/schema/db), quotas, billing/metering, onboarding, and tenant-safe operations
Multi-Tenancy & SaaS Architecture
Overview
Multi-tenancy lets one product serve many customers safely. The hard parts are enforcing isolation everywhere (API, DB, cache, jobs), preventing noisy-neighbor issues, and keeping operations (migrations, billing, support) tenant-aware.
Why This Matters
- Security: prevent cross-tenant data leaks (the #1 existential risk for SaaS)
- Scalability: serve thousands of tenants without exploding ops overhead
- Cost efficiency: shared infra with fair resource allocation
- Velocity: one codebase and deployment model, still customizable per tenant
Core Concepts
1. Tenancy Models
- Single-tenant: strongest isolation, higher cost/ops; good for regulated/large enterprise.
- Multi-tenant: shared everything with logical isolation; best for scale and cost.
- Hybrid: default shared, premium tenants isolated (by schema or database) when needed.
Decision drivers: compliance, data residency, tenant size skew, customization needs, and ops maturity.
2. Data Isolation Strategies
- Row-level (shared DB): add
tenant_id to every table; enforce with DB policies (Postgres RLS) + app-layer scoping.
- Schema-per-tenant: better isolation and per-tenant maintenance, but more migrations/connection management complexity.
- Database-per-tenant: strongest blast-radius control; easiest per-tenant restore; hardest to operate at high tenant counts.
Rule: protect against “forgotten filters” by making isolation enforceable at the lowest layer possible (DB).
3. Tenant Identification & Propagation
- Identify tenant via subdomain, custom domain, header, or JWT claim.
- Validate tenant membership at auth time; never trust a raw header alone.
- Propagate
tenant_id into logs, metrics labels (carefully), and traces for supportability.
4. Resource Quotas & Noisy-Neighbor Controls
- Rate limits per tenant (requests/sec, concurrency, burst).
- Usage limits per tenant (seats, storage, feature entitlements).
- Background work budgets (queue priority, per-tenant concurrency caps, fair scheduling).
5. Tenant Configuration & Customization
- Config store keyed by
tenant_id (limits, features, integrations, branding).
- Feature flags support per-tenant overrides and staged rollouts.
- Avoid tenant-specific branches in core logic; prefer configuration + extension points.
6. Database & App Patterns
- Single source of tenant context: a request-scoped context object; forbid ad-hoc tenant lookups.
- Query scoping: require
tenant_id in repository APIs; add composite indexes like (tenant_id, id).
- Migrations: choose global vs per-tenant scheduling; for large tenants, support phased backfills.
- Connection pooling: consider per-tenant routing; guard against tenant explosion (pool thrash).
7. Billing & Metering
- Emit tenant-scoped usage events (idempotent, deduplicated) to a ledger.
- Separate “raw events” from “billable aggregates”; recompute aggregates from source of truth.
- Align entitlements (plans) with enforcement points (API limits, feature flags, job budgets).
8. Tenant Onboarding & Lifecycle
- Provision: create tenant record, defaults, admin user, and initial data (idempotent).
- Verify: “smoke tests” per tenant (login, create project, run core flow).
- Offboarding: export, deletion/retention policy, key rotation, and access revocation.
Quick Start (Tenant Context Middleware)
import type { Request, Response, NextFunction } from "express";
declare global {
namespace Express {
interface Request {
tenantId?: string;
}
}
}
export function tenantContext(req: Request, res: Response, next: NextFunction) {
const tenantId = req.header("x-tenant-id") ?? req.subdomains?.[0];
if (!tenantId) return res.status(400).json({ error: "Missing tenant" });
// IMPORTANT: verify tenantId is allowed for the authenticated principal.
req.tenantId = tenantId;
next();
}
Production Checklist
Tools & Libraries
| Tool |
Purpose |
| PostgreSQL RLS |
Enforce row-level isolation in the database |
| Prisma / SQLAlchemy / TypeORM |
Tenant-scoped data access patterns |
| Cerbos / OPA |
Authorization and policy evaluation |
| Stripe |
Billing, plans, and invoicing |
| LaunchDarkly / Unleash |
Tenant feature flags and rollouts |
| Redis |
Tenant-aware caching and rate limiting |
Anti-patterns
- “Filter in app only”: no DB enforcement; one missed
tenant_id filter becomes a breach
- Shared resources without limits: one tenant degrades everyone
- Global caches: cached objects without tenant namespace
- Tenant-unaware jobs: background workers processing cross-tenant data accidentally
Real-World Examples
Example: Postgres RLS
- Add
tenant_id to tables; enable RLS; use a session variable (e.g., SET app.tenant_id = ...) and policies like tenant_id = current_setting('app.tenant_id')::uuid.
Example: Tenant-Aware Caching
- Redis keys:
tenant:{tenantId}:users:{userId}; avoid sharing computed results across tenants unless explicitly safe.
Example: Tiered Isolation
- Start with row-level isolation; migrate large tenants to schema-per-tenant later using a dual-write/cutover approach.
Common Mistakes
- Forgetting tenant scoping in admin tools and internal scripts
- Mixing tenant data in logs/traces or exporting tenant IDs without access controls
- Building “customization” as divergent code paths instead of config/extension points
- Applying global migrations/backfills without controlling per-tenant impact
Integration Points
- Authentication/SSO (tenant mapping, domain verification, SCIM)
- Database layer (RLS, migrations, backups, restores)
- Caching + queues (namespacing, fairness)
- Billing + CRM (tenant lifecycle, usage ledger, entitlement enforcement)
Further Reading
1---2name: multi-tenancy-saas3description: Multi-tenancy lets one product serve many customers safely. The hard parts are enforcing isolation everywhere (API, DB, cache, jobs), preventing noisy-neighbor issues, and keeping operations (migratio4---5
6---
7name: Multi-Tenancy & SaaS Architecture
8description: Patterns for building B2B SaaS multi-tenancy: tenant identification, data isolation (RLS/schema/db), quotas, billing/metering, onboarding, and tenant-safe operations
9---
10
11# Multi-Tenancy & SaaS Architecture
12
13## Overview
14
15Multi-tenancy lets one product serve many customers safely. The hard parts are enforcing isolation everywhere (API, DB, cache, jobs), preventing noisy-neighbor issues, and keeping operations (migrations, billing, support) tenant-aware.
16
17## Why This Matters
18
19- **Security**: prevent cross-tenant data leaks (the #1 existential risk for SaaS)
20- **Scalability**: serve thousands of tenants without exploding ops overhead
21- **Cost efficiency**: shared infra with fair resource allocation
22- **Velocity**: one codebase and deployment model, still customizable per tenant
23
24---
25
26## Core Concepts
27
28### 1. Tenancy Models
29
30- **Single-tenant**: strongest isolation, higher cost/ops; good for regulated/large enterprise.
31- **Multi-tenant**: shared everything with logical isolation; best for scale and cost.
32- **Hybrid**: default shared, premium tenants isolated (by schema or database) when needed.
33
34Decision drivers: compliance, data residency, tenant size skew, customization needs, and ops maturity.
35
36### 2. Data Isolation Strategies
37
38- **Row-level** (shared DB): add `tenant_id` to every table; enforce with DB policies (Postgres RLS) + app-layer scoping.
39- **Schema-per-tenant**: better isolation and per-tenant maintenance, but more migrations/connection management complexity.
40- **Database-per-tenant**: strongest blast-radius control; easiest per-tenant restore; hardest to operate at high tenant counts.
41
42Rule: protect against “forgotten filters” by making isolation enforceable at the lowest layer possible (DB).
43
44### 3. Tenant Identification & Propagation
45
46- Identify tenant via **subdomain**, **custom domain**, **header**, or **JWT claim**.
47- Validate tenant membership at auth time; never trust a raw header alone.
48- Propagate `tenant_id` into logs, metrics labels (carefully), and traces for supportability.
49
50### 4. Resource Quotas & Noisy-Neighbor Controls
51
52- Rate limits per tenant (requests/sec, concurrency, burst).
53- Usage limits per tenant (seats, storage, feature entitlements).
54- Background work budgets (queue priority, per-tenant concurrency caps, fair scheduling).
55
56### 5. Tenant Configuration & Customization
57
58- Config store keyed by `tenant_id` (limits, features, integrations, branding).
59- Feature flags support per-tenant overrides and staged rollouts.
60- Avoid tenant-specific branches in core logic; prefer configuration + extension points.
61
62### 6. Database & App Patterns
63
64- **Single source of tenant context**: a request-scoped context object; forbid ad-hoc tenant lookups.
65- Query scoping: require `tenant_id` in repository APIs; add composite indexes like `(tenant_id, id)`.
66- Migrations: choose global vs per-tenant scheduling; for large tenants, support phased backfills.
67- Connection pooling: consider per-tenant routing; guard against tenant explosion (pool thrash).
68
69### 7. Billing & Metering
70
71- Emit tenant-scoped usage events (idempotent, deduplicated) to a ledger.
72- Separate “raw events” from “billable aggregates”; recompute aggregates from source of truth.
73- Align entitlements (plans) with enforcement points (API limits, feature flags, job budgets).
74
75### 8. Tenant Onboarding & Lifecycle
76
77- Provision: create tenant record, defaults, admin user, and initial data (idempotent).
78- Verify: “smoke tests” per tenant (login, create project, run core flow).
79- Offboarding: export, deletion/retention policy, key rotation, and access revocation.
80
81## Quick Start (Tenant Context Middleware)
82
83```typescript
84import type { Request, Response, NextFunction } from "express";
85
86declare global {
87 namespace Express {
88 interface Request {
89 tenantId?: string;
90 }
91 }
92}
93
94export function tenantContext(req: Request, res: Response, next: NextFunction) {
95 const tenantId = req.header("x-tenant-id") ?? req.subdomains?.[0];
96 if (!tenantId) return res.status(400).json({ error: "Missing tenant" });
97
98 // IMPORTANT: verify tenantId is allowed for the authenticated principal.
99 req.tenantId = tenantId;
100 next();
101}
102```
103
104## Production Checklist
105
106- [ ] Tenant isolation enforced at DB layer where possible (e.g., Postgres RLS)
107- [ ] Tenant context is required for all reads/writes (API, jobs, CLI tools)
108- [ ] Caches/queues/storage are tenant-aware (namespacing, prefixes, partitioning)
109- [ ] Quotas/rate limits implemented and monitored per tenant
110- [ ] Tenant-aware observability (logs/traces) and support tooling exist
111- [ ] Onboarding/offboarding are automated and idempotent
112- [ ] Billing/metering events are tenant-scoped with dedupe/idempotency keys
113
114## Tools & Libraries
115
116| Tool | Purpose |
117|------|---------|
118| PostgreSQL RLS | Enforce row-level isolation in the database |
119| Prisma / SQLAlchemy / TypeORM | Tenant-scoped data access patterns |
120| Cerbos / OPA | Authorization and policy evaluation |
121| Stripe | Billing, plans, and invoicing |
122| LaunchDarkly / Unleash | Tenant feature flags and rollouts |
123| Redis | Tenant-aware caching and rate limiting |
124
125## Anti-patterns
126
1271. **“Filter in app only”**: no DB enforcement; one missed `tenant_id` filter becomes a breach
1282. **Shared resources without limits**: one tenant degrades everyone
1293. **Global caches**: cached objects without tenant namespace
1304. **Tenant-unaware jobs**: background workers processing cross-tenant data accidentally
131
132## Real-World Examples
133
134### Example: Postgres RLS
135
136- Add `tenant_id` to tables; enable RLS; use a session variable (e.g., `SET app.tenant_id = ...`) and policies like `tenant_id = current_setting('app.tenant_id')::uuid`.
137
138### Example: Tenant-Aware Caching
139
140- Redis keys: `tenant:{tenantId}:users:{userId}`; avoid sharing computed results across tenants unless explicitly safe.
141
142### Example: Tiered Isolation
143
144- Start with row-level isolation; migrate large tenants to schema-per-tenant later using a dual-write/cutover approach.
145
146## Common Mistakes
147
1481. Forgetting tenant scoping in admin tools and internal scripts
1492. Mixing tenant data in logs/traces or exporting tenant IDs without access controls
1503. Building “customization” as divergent code paths instead of config/extension points
1514. Applying global migrations/backfills without controlling per-tenant impact
152
153## Integration Points
154
155- Authentication/SSO (tenant mapping, domain verification, SCIM)
156- Database layer (RLS, migrations, backups, restores)
157- Caching + queues (namespacing, fairness)
158- Billing + CRM (tenant lifecycle, usage ledger, entitlement enforcement)
159
160## Further Reading
161
162- [Multi-Tenant SaaS Patterns (AWS)](https://aws.amazon.com/partners/programs/saas/)
163- [Azure Multi-Tenant Guidance](https://learn.microsoft.com/azure/architecture/guide/multitenant/)
164- [PostgreSQL Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)