A deep dive into building a robust, production-ready multi-tenant SaaS application using Django covering schema isolation, middleware, routing, and deployment patterns.
Multi-tenancy is one of those architectural decisions that can make or break a SaaS product. Get it wrong early and you'll spend months refactoring. Get it right and you'll have a system that scales gracefully as you onboard hundreds or thousands of customers.
What is Multi-tenancy?
In a multi-tenant application, a single instance of the software serves multiple customers (tenants). Each tenant's data is isolated from others, but the infrastructure is shared. The three classic strategies are: shared database with shared schema, shared database with separate schemas, and separate databases per tenant.
Choosing the Right Isolation Strategy
- Shared Schema simplest to implement, cheapest to run. Row-level filtering via a tenant_id field. Best for early-stage products.
- Separate Schemas (PostgreSQL) each tenant gets its own schema within the same database. Great middle ground.
- Separate Databases maximum isolation. High operational overhead. Usually reserved for enterprise clients.
Setting Up the Project
We'll use django-tenants, which leverages PostgreSQL schemas under the hood. Start by installing it and configuring your database backend.
pip install django-tenants psycopg2-binaryUpdate your settings.py to swap the database engine and add the tenant middleware:
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': 'saas_db',
'USER': 'postgres',
'PASSWORD': 'your_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
# ... rest of your middleware
]Tenant and Domain Models
Every tenant needs a model that maps to a PostgreSQL schema and a domain model for URL routing.
# tenants/models.py
from django_tenants.models import TenantMixin, DomainMixin
from django.db import models
class Tenant(TenantMixin):
name = models.CharField(max_length=100)
created_on = models.DateField(auto_now_add=True)
on_trial = models.BooleanField(default=True)
paid_until = models.DateField(null=True, blank=True)
# Auto-create public schema on save
auto_create_schema = True
class Domain(DomainMixin):
passShared vs. Tenant Apps
Django-tenants distinguishes between public apps (shared across all tenants, live in the public schema) and tenant apps (isolated per tenant, live in each tenant's schema).
# settings.py
SHARED_APPS = [
'django_tenants',
'django.contrib.contenttypes',
'tenants', # your Tenant model lives here
'django.contrib.auth',
'django.contrib.sessions',
]
TENANT_APPS = [
'django.contrib.contenttypes',
'core', # your main business logic
'billing',
'analytics',
]
INSTALLED_APPS = list(SHARED_APPS) + [
app for app in TENANT_APPS if app not in SHARED_APPS
]
TENANT_MODEL = "tenants.Tenant"
TENANT_DOMAIN_MODEL = "tenants.Domain"Provisioning a New Tenant
When a new customer signs up, you'll create a Tenant record and run migrations for their schema automatically.
from tenants.models import Tenant, Domain
def provision_tenant(name: str, schema_name: str, domain_url: str):
tenant = Tenant(
schema_name=schema_name,
name=name,
on_trial=True,
)
tenant.save() # Creates the schema and runs migrations
domain = Domain(
domain=domain_url,
tenant=tenant,
is_primary=True,
)
domain.save()
return tenantProduction Considerations
- Connection Pooling use PgBouncer in transaction mode; session mode doesn't play nicely with schema switching.
- Celery Tasks always capture the current tenant in task kwargs and switch context at the start of each task.
- Caching namespace your Redis keys with the tenant schema name to prevent cross-tenant cache leaks.
- Migrations run migrate_schemas --executor=multiprocessing to parallelize schema migrations across tenants.
- Backups consider per-tenant pg_dump using schema-qualified dumps for fine-grained restore capabilities.
Multi-tenancy in Django is well-solved, but it demands care. Start with the simplest model that fits your current scale, instrument everything from day one, and plan your migration path before you need it.