Building Multi-Tenant SaaS: Database Per Tenant vs Shared Schema
The most important architectural decision in SaaS development: how to isolate tenant data. We compare three approaches with real cost analysis, migration strategies, and code examples.
The Decision That Shapes Everything
When building a SaaS product, the #1 architectural decision is tenant isolation. Get it wrong, and you'll spend months migrating later. There are three approaches, each with clear trade-offs:
Approach 1: Shared Database, Shared Schema (Row-Level Isolation)
Every tenant's data lives in the same tables, distinguished by a tenant_id column. This is the simplest and cheapest approach.
# Django: automatic tenant filtering with middleware
class TenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Set tenant from JWT or subdomain
request.tenant = get_tenant_from_request(request)
return self.get_response(request)
class TenantManager(models.Manager):
def get_queryset(self):
from threading import local
_thread_locals = local()
tenant = getattr(_thread_locals, 'tenant', None)
qs = super().get_queryset()
if tenant:
qs = qs.filter(tenant_id=tenant.id)
return qs
class Order(models.Model):
tenant = models.ForeignKey('Tenant', on_delete=models.CASCADE)
# ... other fields
objects = TenantManager() # All queries auto-filteredPros: Simple, cheap ($50-200/month for one database), easy to deploy, shared connection pool. Cons: Risk of data leaks if you forget a filter, noisy neighbor problem (one tenant's heavy queries affect others), harder compliance (GDPR data deletion).
Approach 2: Shared Database, Separate Schemas (PostgreSQL Schemas)
Each tenant gets their own PostgreSQL schema within a single database. Tables are identical but isolated by namespace.
# Using django-tenants library
# Each request sets the schema based on subdomain
# settings.py
DATABASE_ROUTERS = ['django_tenants.routers.TenantSyncRouter']
# Middleware sets schema per request
# acme.yourapp.com -> schema 'acme'
# globex.yourapp.com -> schema 'globex'
# SQL equivalent:
# SET search_path TO 'acme'; -- all queries now hit acme's tables
# SELECT * FROM orders; -- returns only acme's ordersPros: Strong isolation without extra databases, easy data deletion (DROP SCHEMA CASCADE), separate indexes per tenant. Cons: Migration complexity (must migrate every schema), connection pooling is harder, max ~500 tenants before performance degrades.
Approach 3: Database Per Tenant
Each tenant gets their own database. Maximum isolation, maximum operational complexity.
Pros: Complete isolation, independent scaling, easy compliance, can offer dedicated infrastructure to enterprise clients. Cons: Expensive ($50-200 per tenant per month), connection pooling nightmare, migrations across hundreds of databases, cross-tenant analytics requires a data warehouse.
Decision Matrix: Which Approach to Choose
| Factor | Shared Schema | Separate Schemas | Separate DBs |
|---|---|---|---|
| Monthly cost (100 tenants) | $100-200 | $200-500 | $5,000-20,000 |
| Tenant count limit | Unlimited | ~500 | ~100 (manageable) |
| Data isolation | Weak (row-level) | Strong (schema-level) | Complete |
| Compliance (GDPR/SOC2) | Difficult | Moderate | Easy |
| Noisy neighbor risk | High | Medium | None |
| Best for | B2C, SMB SaaS | Mid-market SaaS | Enterprise SaaS |
Our Recommendation: Start Shared, Migrate Up
Start with shared schema + row-level isolation. It gets you to market fast and handles 90% of use cases. When you land enterprise clients who need isolation guarantees, offer separate schemas or databases as a premium tier.
The key is designing your models with a tenant_id from day one. This makes migration to any approach possible later without rewriting your application.
The best multi-tenant architecture is the one that matches your current stage. A startup with 10 tenants using database-per-tenant is paying 10x what they should.
— alokknight Engineering
