Most SaaS applications need multi-tenancy — the ability to serve multiple organizations (tenants) from a single application, with data isolation between them. Django supports all three main patterns. The choice between them determines your isolation guarantees, scalability ceiling, and operational complexity.
The Three Patterns
1. Shared Schema with tenant_id (Row-Level Tenancy)
Every tenant's data lives in the same tables, distinguished by a tenant_id foreign key.
class Tenant(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Project(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=["tenant", "created_at"]),
]
Every query must filter by tenant. The risk: forgetting the filter returns another tenant's data. Mitigate this with a custom manager:
from django.db import models
class TenantManager(models.Manager):
def for_tenant(self, tenant):
return self.filter(tenant=tenant)
class Project(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
objects = TenantManager()
Middleware that sets the current tenant on the request:
# middleware.py
class TenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
subdomain = request.get_host().split(".")[0]
try:
request.tenant = Tenant.objects.get(slug=subdomain)
except Tenant.DoesNotExist:
request.tenant = None
return self.get_response(request)
Use shared schema when: - You have fewer than 500 tenants - Isolation requirements are low (no regulatory requirement for data separation) - You want the simplest possible implementation - Tenants have similar data volumes
Risks:
- A missing tenant filter in one query leaks data across tenants
- A slow query from one tenant affects all tenants
- Backup/restore for a single tenant requires filtered exports
2. Separate PostgreSQL Schemas (django-tenants)
Each tenant gets their own PostgreSQL schema — a namespace within the same database. Tables are identical in structure but completely isolated by schema.
pip install django-tenants
# settings.py
INSTALLED_APPS = [
"django_tenants",
# ...
]
DATABASES = {
"default": {
"ENGINE": "django_tenants.postgresql_backend",
# Standard PostgreSQL config
}
}
DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"]
TENANT_MODEL = "tenants.Client"
TENANT_DOMAIN_MODEL = "tenants.Domain"
SHARED_APPS = [
"django_tenants",
"tenants",
"django.contrib.auth",
# Apps whose tables live in the public schema
]
TENANT_APPS = [
"projects",
"invoices",
# Apps whose tables are replicated per tenant schema
]
django-tenants automatically switches the PostgreSQL search_path based on the current tenant. Queries hit tenant-specific tables without any changes to your code.
# Creating a tenant
from tenants.models import Client, Domain
tenant = Client(name="Acme Corp", schema_name="acme", paid_until="2027-01-01")
tenant.save() # Creates the acme schema and runs all migrations in it
domain = Domain(domain="acme.yourapp.com", tenant=tenant, is_primary=True)
domain.save()
Use separate schemas when: - Regulatory requirements mandate data isolation (healthcare, finance, legal) - Tenants need independent backup/restore capability - You have tenants who pay for guaranteed isolation - Tenant count is in the hundreds (not tens of thousands)
Risks:
- Schema migrations must run across all tenant schemas — slow at scale
- django-tenants adds complexity to deployment and management commands
- Cross-tenant queries (aggregate analytics) require accessing multiple schemas
3. Subdomain Routing (Application-Level Routing)
Map subdomains to tenants in middleware and use either shared or separate schemas underneath. The routing is the same regardless of the data isolation pattern:
# middleware.py
class SubdomainTenantMiddleware:
def __call__(self, request):
host = request.get_host().lower()
# host = "acme.yourapp.com"
subdomain = host.split(".")[0] # "acme"
if subdomain not in ("www", "yourapp"):
try:
request.tenant = Tenant.objects.select_related().get(slug=subdomain)
except Tenant.DoesNotExist:
from django.http import Http404
raise Http404("Tenant not found")
return self.get_response(request)
For django-tenants, subdomain routing is built-in via the Domain model.
Use subdomain routing when:
- Your product's branding requires acme.yourapp.com patterns
- You want to support custom domains (map crm.acme.com → Acme's tenant)
- You are building a white-label product
Decision Framework
| Factor | Shared Schema | Separate Schemas | Full Isolation |
|---|---|---|---|
| Tenant count | Hundreds–thousands | Tens–hundreds | Any |
| Data isolation requirement | Low | Medium–high | Strict (regulatory) |
| Setup complexity | Low | Medium | High |
| Cross-tenant analytics | Easy | Hard | Very hard |
| Per-tenant backup | Hard | Easy | Easy |
| Migration time at scale | Fast | Slow (runs N times) | Depends |
Hybrid Pattern: Shared Database, Encrypted Columns
For high-compliance scenarios without separate schemas, field-level encryption provides isolation:
from django_encryption.fields import EncryptedTextField
class PatientRecord(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
diagnosis = EncryptedTextField() # Encrypted at rest
Each tenant's data is encrypted with a tenant-specific key. A database breach exposes ciphertext, not plaintext. Key management (typically in a secrets manager) is the operational overhead.
What Most SaaS Products Actually Need
For a new SaaS application with under 200 tenants and no regulatory data separation requirement: start with shared schema. It is the simplest, easiest to reason about, and easiest to migrate away from if you outgrow it. Add the composite index (tenant, created_at) on every major table and use a tenant-aware base manager.
If you reach 500+ tenants with performance complaints from large tenants affecting smaller ones, migrate to separate schemas. This is a significant migration — plan for it when you hit 300 tenants, not when you're in the middle of a performance crisis.
If you are building for healthcare (HIPAA), finance (PCI-DSS), or legal sectors from day one: separate schemas from the start.
If you need a multi-tenant Django SaaS application architected correctly — with the right isolation pattern for your specific requirements — hire me as a Django developer or as a full stack developer.