If you're building a B2B SaaS product, you're building a multi-tenant system whether you realize it or not. Salesforce does it. Shopify does it. Every successful SaaS company has solved this problem. And if you get it wrong, you'll either leak customer data or build something that can't scale.
What Is Multi-Tenancy, Really?#
Multi-tenancy is an architecture where a single instance of software serves multiple customers (called tenants) while keeping each tenant's data and configuration completely isolated. Thousands of companies use the same application, but each sees only their own data.
The challenge isn't just technical — it's architectural. Get the isolation model wrong and you're stuck with it. Get the observability wrong and you can't debug production incidents. Get the security wrong and you're on the front page of Hacker News for all the wrong reasons.
Part 1: The Foundational Decision — Data Isolation Model#
Before you write a single line of code, you need to choose how you physically separate tenant data. There are three models, and every other decision flows from this one.
Model 1: Separate Database Per Tenant#
Each tenant gets their own dedicated database instance.
Security profile: Strongest possible isolation. Compliance requirements like HIPAA and SOC 2 Type II are much easier to satisfy because you can point to physical separation.
The cost: Every migration runs N times. If you have 500 tenants and need to add a column, you're running 500 migration scripts. Connection pooling is expensive.
When to choose this: Regulated industries where physical isolation is a contractual requirement, or for high-value enterprise contracts.
Model 2: Separate Schema Per Tenant#
One database, but each tenant gets their own schema. All tenants share one database process and connection pool, but the query namespace is separated.
Security profile: Strong isolation within the same database. Accidental cross-tenant queries are blocked by the schema boundary.
The cost: Migrations still run per schema, which gets painful past 50–100 tenants. Schema proliferation creates metadata bloat.
When to choose this: Mid-market SaaS with dozens to low hundreds of tenants.
Model 3: Shared Schema with tenant_id#
One database, one schema, every table has a tenant_id column. All tenant data lives in the same tables, separated only by that column.
Security profile: Weakest isolation by design — you're relying on consistent application-level enforcement. However, with Row Level Security enforced at the database layer, you can build strong guarantees.
The cost: Requires strict discipline across every developer and every query. Tables grow large and need careful composite indexing with tenant_id as the leading column.
When to choose this: High-volume SaaS with hundreds to thousands of tenants, or when operational simplicity and fast tenant onboarding are priorities.
Decision Matrix#
| Factor | Separate DB | Separate Schema | Shared Schema |
|---|---|---|---|
| Data isolation | Strongest | Strong | Weakest (mitigatable) |
| Migration complexity | High (per DB) | Medium (per schema) | Low (once) |
| Operational overhead | Very high | Medium | Low |
| Tenant onboarding speed | Slow (minutes) | Medium (seconds) | Fast (milliseconds) |
| Cost per tenant | High | Medium | Low |
| Max tenants practical | ~100 | ~500 | Unlimited |
| Compliance friendliness | Best | Good | Requires RLS + audit |
My recommendation: For most early-stage B2B SaaS companies with under 200 tenants, start with shared schema plus Row Level Security. If you sign an enterprise contract that requires physical separation, build a hybrid model at that point.
Part 2: Tenant Identity — How the System Knows Who You Are#
Before any data isolation can work, every request needs to be tagged with a tenant identity. This tenant context must flow through every layer of your system.
How Tenants Identify Themselves#
Subdomain routing is the cleanest approach for web applications. Each tenant accesses the product through their own subdomain — acme.yourapp.com, globex.yourapp.com.
Custom domains are the premium version. The tenant brings their own domain and points it at your platform via DNS CNAME. This requires automated certificate provisioning (Let's Encrypt via ACME protocol).
API key-based identification is standard for API-first products. The API key maps to a tenant in your database.
JWT claims embed the tenant ID directly in the signed token payload. No database lookup needed.
The Tenant Context Object#
Once you've resolved the tenant from the incoming request, propagate it through your entire application using a request-scoped context object.
Critical design rule: Set the tenant context once, at the boundary of the system, and never re-resolve it deeper in the stack.
Part 3: The Load Balancer Layer#
Routing by Tenant#
Modern load balancers can route requests based on the Host header, allowing you to send different tenants to different backend pools. This becomes valuable when you offer tiered service levels.
Rate Limiting at the Load Balancer Level#
This is your first defense against the noisy neighbor problem — one tenant hammering your infrastructure and degrading performance for everyone else.
Rate limiting at the load balancer level is coarse-grained but highly efficient because it runs before requests hit your application servers. Key rate limits by tenant identity rather than by IP address.
TLS Termination and Certificate Management#
For subdomain-based tenants, a wildcard certificate covers all subdomains. For custom domain tenants, you need automated certificate provisioning triggered when a tenant registers their domain.
Part 4: The Application Layer#
Middleware Stack and Request Lifecycle#
Your middleware stack should resolve and enforce tenant context early, before any business logic runs:
- Resolve the tenant from the request
- Authenticate the user and verify they belong to that tenant
- Apply authorization for the specific action
- Apply per-tenant rate limits
- Only then does the request reach your business logic
The common mistake: verifying that a token is valid without verifying it belongs to the current tenant.
Application-Level Rate Limiting#
More granular than load balancer rate limiting. You can differentiate by endpoint, by tenant tier, and by specific operations. These limits are enforced against Redis, keyed by tenant ID.
Tenant Configuration and Feature Flags#
Store per-tenant settings as structured data in a config table. For major behavioral differences like different ERP integrations, use a strategy pattern at the code level.
Never scatter if tenant_id == 'acme' conditionals through your codebase. This is unmaintainable, untestable, and invisible to the next engineer.
Background Jobs and Queues#
Background jobs are one of the most common sources of multi-tenancy bugs. Every job must carry the tenant ID as a payload field, and must restore tenant context before doing any work. Treat missing tenant ID in a job payload as a hard error.
Part 5: The Database Layer#
Row Level Security#
Row Level Security (RLS) is PostgreSQL's built-in mechanism to enforce per-row access control at the database engine level. Even if your application has a bug and forgets to filter by tenant_id, the database itself enforces the restriction.
Critical configuration detail: Your application should connect as a restricted role that cannot bypass RLS. Superuser and table owner roles bypass RLS policies automatically.
Indexing Strategy for Shared Schema#
The rule: Every frequently-queried column combination needs a composite index with tenant_id as the leading column. An index on status alone will not efficiently serve tenant-scoped queries.
Missing indexes on large shared tables aren't just a performance problem — they're a fairness problem. One tenant with a large dataset and a missing index causes table scans that degrade performance for every other tenant.
Connection Pooling#
Always set the tenant context variable scoped to the transaction, not the session. Session-scoped settings persist to the connection after the transaction ends and can leak tenant context to the next request. This is a subtle but serious security bug.
Schema Migrations at Scale#
For shared schema, use the expand-contract pattern for zero-downtime migrations:
- Add the new column as nullable first and deploy
- Backfill existing rows with a background job
- Add the NOT NULL constraint once backfill is verified
- Remove the old column in a later release
Never add a NOT NULL column without a default value on a large live table — it will lock the table for the duration of the backfill.
Part 6: The Cache Layer#
Key Namespacing#
Every cache key must include the tenant ID as a namespace prefix. The pattern is tenant:{tenant_id}:{resource_type}:{resource_id}.
Missing the tenant namespace is a silent bug — Tenant A's product catalog gets cached, Tenant B makes the same request, the cache returns Tenant A's data. Both tenants receive valid-looking responses.
Make key namespacing a required code review checklist item.
Cache Invalidation#
Write-through invalidation — explicitly deleting the Redis key on data change — is more reliable than TTL expiry for correctness. For security-relevant configuration (like disabling a tenant's access), TTL expiry is not acceptable. A tenant deactivated in the database should not continue to function because their config is still cached.
Part 7: Observability and Incident Response#
Tagging Everything with Tenant ID#
Every log line, every metric, every distributed trace span must include the tenant ID. When a customer calls support saying their orders aren't loading, your on-call engineer should be able to pull up that tenant's request traces, error logs, and query times in under two minutes.
Key Metrics to Track Per Tenant#
- Request volume and latency: P95 latency broken down by tenant
- Error rate by tenant: One tenant hitting elevated errors can indicate a data integrity problem or an attempted attack
- Database query time by tenant: A tenant with a large dataset and a missing index shows up here first
- Job queue depth by tenant: A tenant's background jobs piling up indicates a processing bottleneck
Detecting Cross-Tenant Data Leakage#
Build automated tests that create two test tenants, seed data for one, and verify the other cannot access it — through both direct queries and API endpoints.
One important subtlety: When Tenant B tries to access Tenant A's resource by ID, the response should be 404, not 403. Returning 403 confirms the resource exists.
Part 8: Tenant Onboarding#
Fully automated onboarding is a competitive advantage. The onboarding pipeline should be a sequence of automated steps:
- Create the tenant record
- Seed default configuration
- Create the admin user
- Send the invite email
- Set up monitoring dashboards for the new tenant
- Mark the tenant active
The entire sequence should complete in seconds. Measure your provisioning time and treat it as a product metric. Under five seconds is excellent. Over a minute is a problem worth solving.
Part 9: Common Mistakes and How to Avoid Them#
Forgetting tenant context in async callbacks — An event listener or timer callback runs outside the request lifecycle. Always pass tenant ID explicitly to any async operation.
Caching without tenant namespace — Users occasionally see another customer's data. This is a serious security incident. Make it a code review requirement.
Rate limiting by IP instead of by tenant — IP-based limits protect against external attackers but do nothing for a legitimate tenant generating excessive load.
Missing composite indexes — Queries get slower as total data volume grows. Every query filtering by tenant_id plus another column needs a composite index with tenant_id leading.
Migrations that lock tables — Always use the expand-contract pattern for schema changes on live tables.
Not testing tenant isolation — Cross-tenant bugs don't surface in unit tests. They only surface in integration tests with multiple tenants. Tenant isolation tests belong in your CI pipeline as a required gate.
Session-level context setting with connection pooling — Always use transaction-scoped settings when working with connection poolers.
The Checklist: What to Build and When#
- Architecture decision — Choose data isolation model based on tenant count, compliance requirements, and operational capacity. Hardest to reverse.
- Tenant identity — Resolve tenant on every request. Propagate context through the entire request lifecycle. Verify authenticated users belong to the resolved tenant.
- Load balancer — Route by Host header. Apply coarse-grained per-tenant rate limits. Handle TLS automatically.
- Application — Enforce tenant context in middleware before any business logic. Store customization in config tables not code conditionals. Always carry tenant ID in background job payloads.
- Database — Enable RLS. Use transaction-scoped context variables. Create composite indexes with
tenant_idleading. Connect as a non-superuser role. Use expand-contract for zero-downtime migrations. - Cache — Namespace every key with
tenant_id. Invalidate explicitly on write. - Observability — Tag every log, metric, and trace with
tenant_id. Alert on per-tenant latency, error rate, and queue depth. Run automated cross-tenant isolation tests on every deploy.
Final Thoughts#
Building multi-tenancy correctly is not glamorous work, but it is the foundation everything else rests on. The time you invest in getting isolation, context propagation, and observability right in the first year will save you from the security incident, the performance crisis, and the 3am debugging session you never see coming until it's too late.
Multi-tenancy is one of those decisions that matters early. Get the isolation model right. Build tenant context propagation into your middleware from day one. Tag everything with tenant ID. Test tenant isolation in CI. The rest you can figure out as you go.
If you're building a B2B SaaS product, you're solving this problem whether you want to or not. You might as well solve it well.