Multi-Tenant SaaS
Multi-tenant platform with PostgreSQL Row-Level Security for tenant data isolation, a dedicated control plane database for tenant metadata, Redis for per-tenant rate limiting and caching, and Kafka for tenant event streaming and audit trail.
Description
Multi-tenancy requires an explicit choice between three isolation models, each with different operational complexity and tenant density trade-offs:
1. Shared schema with Row-Level Security (RLS): all tenants share tables, RLS policies
enforce isolation. Maximum density (10k+ tenants per database), lowest operational
cost, hardest to debug isolation failures. Appropriate up to 1000–2000 active tenants.
2. Schema-per-tenant (PostgreSQL namespaces): one PostgreSQL schema per tenant within
a shared database. Good isolation, clean migration story per-tenant, but schema
migration across all tenants requires explicit orchestration. Practical up to 5000
tenants per database instance.
3. Database-per-tenant: maximum isolation, independent scaling, but highest operational
cost. One connection pool per tenant. Appropriate for large enterprise customers or
regulated data requirements.
This composition implements shared schema with RLS plus a control plane PostgreSQL for tenant metadata. The control plane is the authoritative registry of tenants, their configuration, and their provisioning state: separate from the tenant data plane to prevent noisy-neighbor effects.
Use Cases
- ·B2B SaaS serving 10–5000 tenants
- ·Developer platforms with isolated workspace per team or organization
- ·Compliance-sensitive platforms where tenant data boundaries must be auditable
- ·Platforms with shared infrastructure but per-tenant customization
- ·Products transitioning from single-tenant to multi-tenant
Scale Profile
Entry Point
2+ tenants requiring data isolation
Sweet Spot
10–2000 active tenants, shared schema + RLS
Scaling Ceiling
Shared schema ceiling: ~2000 concurrently active tenants before query plan noise. Schema-per-tenant ceiling: ~5000 tenants. Database-per-tenant: theoretically unbounded but operationally expensive beyond 500 databases.
Typical RPS
100–20k RPS depending on tenant count and per-tenant load
Architecture Nodes (7)
Tenant-specific web and API clients. Each request carries a tenant identifier (JWT claim or subdomain header).
Extracts tenant_id from JWT or subdomain. Validates tenant exists and is active via control plane lookup. Routes to application tier. Enforces per-tenant rate limits using Redis.
Stateless application servers. Sets PostgreSQL session parameter app.current_tenant_id before each query: RLS policies use this to filter rows. All ORM queries inherit the session-level tenant context.
Separate PostgreSQL instance for tenant registry. Stores: tenant_id, name, status, isolation_model, created_at, plan, configuration. Never co-located with tenant data. Read on every request: connection pooled aggressively.
Data plane PostgreSQL with Row-Level Security. All tenant data tables have RLS policies on tenant_id column. BYPASSRLS role is strictly controlled. Migration tooling must apply policies to every new table.
Per-tenant key namespace: tenant:{tenant_id}:{key}. Rate limiting per tenant (sliding window). Session cache, tenant feature flags, and short-lived computed results. Invalidation events keyed to tenant_id.
Tenant event streaming. Events include tenant_id in header and payload. Used for audit trail, cross-tenant analytics, and webhook delivery. One consumer group per downstream consumer.
Dependencies (7)
4 critical path edges. Failure on these directly degrades user-facing requests.
Tenant-scoped API requests
All requests. JWT contains tenant_id claim. Subdomain routing maps *.app.example.com to tenant_id. Gateway validates tenant on every request.
Timeout: 30s
Per-tenant rate limit check
Sliding window rate limit per tenant per endpoint. Key: ratelimit:{tenant_id}:{endpoint}. Blocks if limit exceeded. Returns 429 with Retry-After header.
Timeout: 50ms
Tenant validation
Validates tenant status on each request. Cached in Redis with 60-second TTL to reduce control plane load. Critical path: if control plane is down, new requests cannot be validated.
Timeout: 200ms
Authenticated requests
Gateway forwards request with validated tenant_id header to application tier.
Timeout: 30s
Tenant data reads/writes
App sets SET LOCAL app.current_tenant_id = $tenant_id per session. RLS policies enforce row-level filtering. Missing or wrong tenant_id returns empty results, not an error: a configuration mistake is silent.
Timeout: 5s
Tenant-scoped cache
All Redis keys prefixed with tenant:{tenant_id}:. Cache invalidation on write is per-tenant. No cross-tenant key pollution possible.
Timeout: 100ms
Tenant domain events
Publishes tenant-scoped domain events for audit trail and webhook delivery. Events include tenant_id for downstream fanout and filtering.
Failure Propagation
How a failure in one component cascades through the system. Each path documents the mechanism and the mitigation that breaks the chain.
Mechanism
Large tenant running complex analytical queries holds shared locks or saturates autovacuum. Other tenants experience latency increases. In shared schema, one tenant's pg_stat_activity overwhelms the query planner's statistics.
Mitigation
Set statement_timeout per session. Implement per-tenant connection limits at PgBouncer. Alert when any single tenant's queries exceed 30% of total active connections.
Mechanism
Control plane failure means tenant validation fails. New requests cannot be authenticated. Cached tenant state in Redis (60s TTL) provides brief degraded service. After TTL expiry, all requests fail.
Mitigation
Redis TTL acts as a buffer. Deploy control plane with HA. Implement gateway-level fallback: if control plane unreachable and tenant cached, allow request. Alert immediately on control plane degradation.
Mechanism
Missing or incorrect RLS policy on a new table silently returns all rows to any tenant. BYPASSRLS role misuse or missing SET LOCAL call in app means no row-level filtering. Data leak with no error.
Mitigation
Automated schema migration test: assert RLS is enabled on all tenant data tables. CI test: query new table as tenant A and verify tenant B's rows are invisible. Audit BYPASSRLS role grants quarterly.
Scaling Transitions
Inflection points where this architecture begins to degrade and what the recommended evolution looks like.
Query planner statistics and autovacuum cannot keep pace with thousands of tenant-specific row distributions. Query plan quality degrades for tenants with unusual data distributions.
Recommended Action
Migrate to schema-per-tenant isolation model. Each tenant gets a dedicated PostgreSQL schema. Run migrations per-schema with orchestration tool.
Shared database cannot provide per-tenant performance isolation. Enterprise SLA commitments require guaranteed IOPS and connection capacity.
Recommended Action
Introduce dedicated PostgreSQL instances for large enterprise tenants. Maintain shared-schema pool for smaller tenants. Hybrid isolation model.
Patterns Applied
Architectural Notes
- ·RLS policy must be tested in CI. A missing ENABLE ROW LEVEL SECURITY or FORCE ROW LEVEL SECURITY on a table is a data leak, not a bug: it must be caught before deployment.
- ·Control plane and data plane must be separate databases. Putting tenant metadata in the same database as tenant data means a noisy tenant can affect tenant provisioning.
- ·Per-tenant key namespacing in Redis must be enforced at the service layer. A missing prefix is a cross-tenant data leak with no error signal.
- ·Schema migrations in multi-tenant systems take longer: a migration must run across all tenant schemas or all tenant rows. Plan for migration windows proportional to tenant count.
Confidence
StrongMulti-tenant SaaS with PostgreSQL RLS is documented by Supabase, Citus, and multiple B2B SaaS engineering teams. Operational trade-offs are well-characterized.