Multi-Tenant Database Design: Shared vs Isolated Schemas
The database architecture you choose for multi-tenancy determines your SaaS product's cost structure, security posture, and operational complexity for years. Get it wrong and you're re-architecting under production load. Get it right and you scale to thousands of tenants without architectural rewrites.
This guide compares the two dominant multi-tenant database patterns: shared schema (all tenants in one schema with tenant_id filtering) and isolated schema (separate schema per tenant). We'll cover security implications, performance trade-offs, and decision frameworks.
Shared Schema: One Schema, All Tenants
In a shared schema model, all tenants store data in the same tables within a single database schema. Every table includes a tenant_id (or workspace_id) column, and every query filters by it.
How It Works
CREATE TABLE projects (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name VARCHAR(255),
created_at TIMESTAMPTZ,
INDEX idx_tenant (tenant_id)
);
SELECT * FROM projects WHERE tenant_id = 'abc-123';
Every application query must include the tenant filter. Miss it once and you've created a cross-tenant data leak.
Advantages
- Simplicity: One schema to migrate, one set of indexes to manage, one connection pool.
- Cost efficiency: Maximum resource utilization — idle tenants don't waste dedicated resources.
- Cross-tenant analytics: Aggregate queries across all tenants are trivial.
- Operational simplicity: Schema changes deploy once for all tenants.
Risks
- Data leakage: If a single query omits
tenant_id filtering, you've exposed all tenant data.
- Noisy neighbor: One tenant's heavy query load affects all others unless partitioned.
- Compliance complexity: Hard to prove per-tenant isolation for GDPR/HIPAA audits.
Row-Level Security (RLS): The Shared Schema Safety Net
PostgreSQL's Row-Level Security enforces tenant filtering at the database level, not the application level. Even if your application code forgets the filter, the database blocks cross-tenant access.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Every session sets app.current_tenant on connection. Queries automatically filter by that tenant — no application-level WHERE clause needed.
Why RLS Matters
- Defense in depth: Even with application bugs, database enforces isolation.
- Audit compliance: Policy enforcement logged at DB level.
- Simpler application code: ORMs don't need manual tenant filters in every query.
RLS policies add slight query overhead — typically 5-15% on filtered queries. Mitigate with:
- Index on
tenant_id (critical)
- Table partitioning by
tenant_id for large datasets
- Connection pooling per tenant to cache
current_tenant setting
Isolated Schema: One Schema Per Tenant
Each tenant gets its own PostgreSQL schema within a single database. Tenants are logically separated at the schema level.
CREATE SCHEMA tenant_abc123;
CREATE TABLE tenant_abc123.projects (
id UUID PRIMARY KEY,
name VARCHAR(255),
created_at TIMESTAMPTZ
);
SET search_path TO tenant_abc123;
SELECT * FROM projects;
No tenant_id column needed — isolation is structural.
Advantages
- Strong isolation: Schema-level boundaries reduce cross-tenant risk.
- Per-tenant customization: Add tenant-specific columns or tables without affecting others.
- Easier compliance: Auditors see clear per-tenant boundaries.
- Selective backups: Restore individual tenant schemas without full DB restore.
Disadvantages
- Migration complexity: Schema changes must run for every tenant schema (hundreds or thousands).
- Connection management: Must set
search_path per request or maintain per-tenant connection pools.
- Cross-tenant queries: Aggregate analytics require UNION across all schemas — impractical at scale.
- Schema proliferation: PostgreSQL handles thousands of schemas, but tooling (ORMs, migration tools) often doesn't.
Shared vs Isolated: Side-by-Side
| Dimension | Shared Schema + RLS | Isolated Schema |
| Isolation strength | Medium (enforced by policy) | High (structural) |
| Query performance | Fast with proper indexes | Slightly faster (no filter overhead) |
| Migration complexity | Low (one schema) | High (N schemas) |
| Cost efficiency | Highest | Moderate |
| Cross-tenant analytics | Trivial | Impractical |
| Customization per tenant | Hard | Easy |
| Tenant limit | Thousands+ | Hundreds (practical limit) |
| Compliance clarity | Requires RLS + audit | Clear boundaries |
When to Use Each Pattern
Use Shared Schema When:
- You have 100+ tenants with similar data volume and usage patterns
- Cross-tenant analytics matter — aggregate dashboards, ML training data
- Cost and operational simplicity are priorities over maximum isolation
- Your ORM/framework has strong RLS support (e.g., Supabase, Hasura)
Use Isolated Schema When:
- You have 10-100 tenants with highly variable schemas or compliance needs
- Regulatory compliance demands clear structural isolation (healthcare, finance)
- Per-tenant customization is a product feature (white-label SaaS)
- Tenant data must be independently exportable/deletable for GDPR compliance
Hybrid Approach: Shard by Tenant Tier
Many mature SaaS products use a hybrid:
- Free/starter tenants: Shared schema with RLS (cost-optimized)
- Enterprise tenants: Isolated schema or dedicated database (compliance + SLA guarantees)
This gives you cost efficiency at scale while meeting enterprise requirements. Implementation requires routing logic based on tenant metadata.
Implementation Checklist
For Shared Schema:
- ✅ Enable RLS on all tenant-scoped tables
- ✅ Index
tenant_id on every table
- ✅ Set
current_tenant at connection/session start
- ✅ Audit all queries to ensure
tenant_id is never omitted
- ✅ Implement tenant-based partitioning for large tables
- ✅ Log RLS policy violations for security monitoring
For Isolated Schema:
- ✅ Automate schema creation on tenant signup
- ✅ Build migration runner that applies changes to all schemas
- ✅ Set
search_path per request based on tenant context
- ✅ Implement per-tenant backup and restore procedures
- ✅ Monitor schema count — plan sharding before hitting DB limits
For the broader SaaS architecture context, see Tenant Data Isolation Patterns and Anti-Patterns. For authentication/authorization that integrates with these patterns, see OAuth 2.0 and RBAC for SaaS.
FAQs
What is the main difference between shared schema and isolated schema in multi-tenant SaaS?
In a shared schema model, all tenants store data in the same tables with a tenant_id column to filter data per tenant. In an isolated schema model, each tenant gets a separate database schema — tables are structurally separated, not logically filtered. Shared schema optimizes for cost and simplicity; isolated schema optimizes for compliance and per-tenant customization.
What is Row-Level Security and why does it matter for SaaS?
Row-Level Security (RLS) is a database feature (PostgreSQL, Azure SQL) that enforces filtering policies at the database level. It ensures that even if application code forgets to filter by tenant_id, the database still blocks cross-tenant access. This provides defense-in-depth for shared schema architectures and simplifies compliance audits.
How many tenants can a shared schema support?
With proper indexing, partitioning, and RLS policies, a shared schema can support thousands to tens of thousands of tenants in a single PostgreSQL database. The practical limit is usually query performance and operational complexity, not the database itself. Add horizontal sharding when a single DB can no longer handle the combined workload.
When should I use isolated schema instead of shared schema?
Use isolated schema when you have 10-100 tenants with strong compliance requirements (HIPAA, GDPR), need per-tenant schema customization, or must provide independently exportable tenant data. If you're scaling to hundreds of similar tenants, shared schema with RLS is simpler and more cost-effective.