workaboutcontactblogs
All articles

CreatingaProductionGradeMultitenancyAppinDjango

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.

bash
pip install django-tenants psycopg2-binary

Update your settings.py to swap the database engine and add the tenant middleware:

python
# 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.

python
# 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):
    pass

Shared 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).

python
# 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.

python
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 tenant

Production 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.