Building a multi-tenant SaaS application requires fundamental architecture decisions that impact scalability, security, and cost. As Rishikesh Baidya, our CTO, has designed dozens of SaaS platforms through our web development services, the patterns covered here have been battle-tested across diverse use cases.
Multi-Tenancy Models
The first decision in SaaS architecture is choosing your tenancy model. Each has distinct trade-offs.
Choosing Your Model
| Factor | Single-Tenant | Multi-Tenant | Hybrid |
|---|---|---|---|
| Cost per tenant | High | Low | Variable |
| Data isolation | Complete | Logical | Tiered |
| Customization | Unlimited | Limited | Tier-based |
| Operational complexity | High (many instances) | Medium (shared) | Highest |
| Best for | Enterprise | SMB/Self-service | Mixed market |
Database Architecture Patterns
Database design is the most critical multi-tenancy decision. Choose based on isolation requirements, compliance needs, and scale targets.
Pattern 1: Shared Database, Shared Schema
All tenants share tables, distinguished by tenant_id column.
CREATE TABLE users (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant (tenant_id)
);-- CRITICAL: Always filter by tenant_id
SELECT * FROM users WHERE tenant_id = ? AND email = ?;
PostgreSQL RLS example:
ALTER TABLE users ENABLE ROW LEVEL SECURITY;CREATE POLICY tenant_isolation ON users
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Pattern 2: Shared Database, Separate Schemas
Each tenant gets their own schema within a shared database.
-- Create schema per tenant
CREATE SCHEMA tenant_abc;
CREATE TABLE tenant_abc.users (...);-- tenant_xyz gets separate schema
CREATE SCHEMA tenant_xyz;
CREATE TABLE tenant_xyz.users (...);
- Benefits:
- Stronger isolation than shared schema
- Easier per-tenant backup/restore
- Simpler data migration between tenants
Pattern 3: Separate Databases
Maximum isolation—each tenant gets a dedicated database.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ tenant_abc_db │ │ tenant_xyz_db │ │ tenant_123_db │
│ │ │ │ │ │
│ users │ │ users │ │ users │
│ orders │ │ orders │ │ orders │
│ products │ │ products │ │ products │
└─────────────────┘ └─────────────────┘ └─────────────────┘Application Layer Implementation
Tenant Resolution Strategies
- Resolution approaches:
- Subdomain:
acme.yourapp.com→ tenant: acme - Path:
yourapp.com/acme/dashboard→ tenant: acme - Header:
X-Tenant-ID: acme(API-first products) - JWT claim: Tenant embedded in authentication token
Request Context Pattern
import { AsyncLocalStorage } from 'async_hooks';interface Tenant {
id: string;
slug: string;
plan: 'free' | 'pro' | 'enterprise';
databaseUrl?: string;
}
class TenantContext {
private static storage = new AsyncLocalStorage();
static getCurrentTenant(): Tenant {
const tenant = this.storage.getStore();
if (!tenant) {
throw new Error('No tenant context - middleware not configured');
}
return tenant;
}
static run(tenant: Tenant, fn: () => T): T {
return this.storage.run(tenant, fn);
}
}
// Express middleware
const tenantMiddleware = async (req, res, next) => {
const tenantSlug = req.subdomains[0] || 'default';
const tenant = await resolveTenant(tenantSlug);
if (!tenant) {
return res.status(404).json({ error: 'Tenant not found' });
}
TenantContext.run(tenant, () => next());
};
Secure Data Access Layer
abstract class TenantScopedRepository {
protected abstract tableName: string; private get tenantId(): string {
return TenantContext.getCurrentTenant().id;
}
async findAll(conditions?: Partial): Promise {
// Tenant filter is ALWAYS applied automatically
return this.db.query(
SELECT * FROM ${this.tableName} WHERE tenant_id = $1,
[this.tenantId]
);
}
async create(data: Omit): Promise {
// Tenant ID is ALWAYS injected automatically
return this.db.query(
INSERT INTO ${this.tableName} (tenant_id, ...) VALUES ($1, ...),
[this.tenantId, ...]
);
}
}
Isolation Strategies
Compute Isolation Options
Data Isolation Hierarchy
From least to most isolated:
Scaling Patterns
Horizontal Scaling Architecture
┌─────────────────┐
│ Load Balancer │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ App Pod 1 │ │ App Pod 2 │ │ App Pod N │
│ (Stateless) │ │ (Stateless) │ │ (Stateless) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└──────────────────┴──────────────────┘
│
┌──────┴──────┐
▼ ▼
┌──────────┐ ┌───────────┐
│ Primary │ │ Replica │
│ DB │──│ DB │
└──────────┘ └───────────┘- Key principles:
- Stateless application tier (session in Redis/database)
- Horizontal pod autoscaling based on load
- Database read replicas for query distribution
Tenant Sharding
For large scale, shard tenants across database clusters:
interface ShardConfig {
id: number;
connectionUrl: string;
tenantRange: [number, number];
}function getShardForTenant(tenantId: string): ShardConfig {
const hash = consistentHash(tenantId);
const shardId = hash % totalShards;
return shardConfigs.find(s => s.id === shardId);
}
// Route tenant to correct shard
const tenant = await TenantContext.getCurrentTenant();
const shard = getShardForTenant(tenant.id);
const db = getConnection(shard.connectionUrl);
Tiered Infrastructure
Security Checklist
- Tenant ID verified on every request (middleware)
- No cross-tenant data access possible (test with penetration testing)
- Audit logging for sensitive operations
- Encryption at rest (database-level or application-level)
- Encryption in transit (TLS everywhere)
- Per-tenant encryption keys for enterprise (optional)
- Data residency compliance (store data in required regions)
- Right to deletion workflow implemented
Operational Considerations
Per-Tenant Monitoring
Track metrics at the tenant level for debugging and capacity planning:
Tenant Lifecycle
- Automate the entire lifecycle:
- Provisioning - Database setup, initial data, DNS configuration
- Onboarding - Welcome emails, sample data, getting started guides
- Offboarding - Data export, grace period, secure deletion
Real-World Example
Our Intranet product uses a hybrid approach: shared database with schema separation for standard customers, and dedicated databases for enterprise customers with compliance requirements. This balances cost efficiency with enterprise security needs.
Building a SaaS Platform?
We help companies design and implement scalable multi-tenant architectures. From early-stage startups to enterprise platforms, we've built SaaS systems that scale.
Discuss Your Architecture →