Django Multi-Tenancy: Building SaaS Applications

Multi-tenancy enables single Django application serving multiple customers (tenants) with complete data isolation maintaining security and privacy crucial for SaaS (Software as a Service) platforms where numerous organizations share infrastructure. Traditional single-tenant applications require separate deployments for each customer consuming significant resources while multi-tenant architecture consolidates customers onto shared infrastructure reducing costs dramatically. Without proper multi-tenancy, SaaS providers must maintain hundreds of separate application instances creating deployment nightmares, increasing server costs exponentially, and complicating updates requiring changes across every customer environment. Multi-tenancy provides tenant isolation ensuring customers cannot access other tenants' data while sharing application code, database connections, and server resources optimizing efficiency. Real-world use cases include project management platforms serving thousands of companies, CRM systems hosting multiple organizations, e-commerce platforms with independent stores, educational platforms supporting different institutions, and accounting software serving numerous businesses maintaining separate financial data. Multi-tenancy strategies include schema-based isolation creating separate PostgreSQL schemas per tenant, database-per-tenant isolating tenants completely with separate databases, and shared-schema with tenant column filtering using TenantID field throughout Django models maintaining single database. This comprehensive guide explores multi-tenancy with Django 6.0 including understanding multi-tenancy architectures and isolation strategies, implementing django-tenants for schema-based multi-tenancy, configuring subdomain routing for tenant identification, creating shared and tenant-specific models, implementing tenant-aware views and middleware, managing tenant databases and migrations, implementing tenant authentication and permissions, handling static files per tenant, deploying multi-tenant applications to production, and best practices for scalable SaaS architectures throughout Django development from initial tenant setup through enterprise SaaS platforms serving millions of users across thousands of tenant organizations integrated with production deployment.
Multi-Tenancy Architecture Strategies
Multi-tenancy architectures range from complete isolation with separate databases to shared databases with tenant filtering each offering different tradeoffs between isolation, performance, and cost. Schema-based isolation using PostgreSQL schemas provides strong isolation while maintaining single database connection. Understanding architecture choices integrated with PostgreSQL setup enables selecting appropriate strategy for SaaS application requirements balancing security, scalability, and cost.
| Strategy | Isolation Level | Scalability | Cost | Best For |
|---|---|---|---|---|
| Separate Databases | Complete isolation | Limited by connections | High | Few large enterprise tenants |
| Separate Schemas | Strong isolation | Good (100s tenants) | Medium | Most SaaS applications |
| Shared Schema + Filter | Application-level | Excellent (1000s tenants) | Low | High-volume micro-tenants |
Implementing Schema-Based Multi-Tenancy
Django-tenants provides schema-based multi-tenancy creating separate PostgreSQL schemas for each tenant with automatic schema switching based on subdomain or domain. Tenants share application code and database connection while maintaining complete data isolation through schemas. Understanding django-tenants configuration integrated with Django project structure enables building production-ready SaaS applications maintaining tenant isolation.
# Install django-tenants
pip install django-tenants psycopg2-binary
# Django 6.0 settings.py configuration
# Add to installed apps (order matters!)
INSTALLED_APPS = [
'django_tenants', # Must be first
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin',
'django.contrib.staticfiles',
# Your apps
'customers', # Tenant management app
'myapp', # Your main app
]
# Database configuration (PostgreSQL required)
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': 'saas_database',
'USER': 'postgres',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
}
}
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
# Tenant configuration
TENANT_MODEL = 'customers.Client'
TENANT_DOMAIN_MODEL = 'customers.Domain'
# Public schema (shared across all tenants)
PUBLIC_SCHEMA_URLCONF = 'myproject.urls_public'
# Tenant schemas (per-tenant routes)
ROOT_URLCONF = 'myproject.urls'
# Middleware (order matters!)
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Show public schema in admin
SHOW_PUBLIC_IF_NO_TENANT_FOUND = True
# Tenant and Domain models
# customers/models.py
from django_tenants.models import TenantMixin, DomainMixin
from django.db import models
class Client(TenantMixin):
name = models.CharField(max_length=100)
created_on = models.DateField(auto_now_add=True)
# Shared schema stores tenant metadata
auto_create_schema = True
def __str__(self):
return self.name
class Domain(DomainMixin):
pass
# Example tenant-specific model
# myapp/models.py
from django.db import models
from django.contrib.auth.models import User
class Project(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Task(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
completed = models.BooleanField(default=False)
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
def __str__(self):
return self.title
# Run migrations
# Create public schema
python manage.py migrate_schemas --shared
# Create tenant schema
python manage.py migrate_schemas --tenantCreating and Managing Tenants
Creating tenants involves creating Client and Domain instances with django-tenants automatically creating PostgreSQL schemas and running migrations for tenant-specific tables. Tenant management views handle signup, provisioning, and billing. Understanding tenant lifecycle integrated with Django admin enables building self-service tenant provisioning maintaining automation.
# Creating tenants programmatically
# customers/utils.py
from django_tenants.utils import schema_context, tenant_context
from .models import Client, Domain
from django.contrib.auth.models import User
def create_tenant(tenant_name, domain_url, admin_email, admin_password):
"""
Create new tenant with domain and admin user
"""
# Create tenant
tenant = Client(
schema_name=tenant_name.lower().replace(' ', '_'),
name=tenant_name
)
tenant.save()
# Create domain
domain = Domain()
domain.domain = domain_url
domain.tenant = tenant
domain.is_primary = True
domain.save()
# Create admin user for tenant
with tenant_context(tenant):
admin_user = User.objects.create_superuser(
username='admin',
email=admin_email,
password=admin_password
)
return tenant, domain, admin_user
# Django management command
# customers/management/commands/create_tenant.py
from django.core.management.base import BaseCommand
from customers.utils import create_tenant
class Command(BaseCommand):
help = 'Create a new tenant'
def add_arguments(self, parser):
parser.add_argument('name', type=str)
parser.add_argument('domain', type=str)
parser.add_argument('email', type=str)
parser.add_argument('password', type=str)
def handle(self, *args, **options):
tenant, domain, user = create_tenant(
tenant_name=options['name'],
domain_url=options['domain'],
admin_email=options['email'],
admin_password=options['password']
)
self.stdout.write(
self.style.SUCCESS(
f'Tenant "{tenant.name}" created successfully!\n'
f'Domain: {domain.domain}\n'
f'Schema: {tenant.schema_name}'
)
)
# Usage:
# python manage.py create_tenant "Acme Corp" "acme.example.com" "[email protected]" "password123"
# Tenant signup view
# customers/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import TenantSignupForm
from .utils import create_tenant
def tenant_signup(request):
if request.method == 'POST':
form = TenantSignupForm(request.POST)
if form.is_valid():
try:
tenant, domain, user = create_tenant(
tenant_name=form.cleaned_data['company_name'],
domain_url=f"{form.cleaned_data['subdomain']}.example.com",
admin_email=form.cleaned_data['email'],
admin_password=form.cleaned_data['password']
)
messages.success(
request,
f'Tenant created! Access at https://{domain.domain}'
)
return redirect('tenant_success')
except Exception as e:
messages.error(request, f'Error creating tenant: {e}')
else:
form = TenantSignupForm()
return render(request, 'customers/signup.html', {'form': form})
# List tenants
def list_tenants():
from customers.models import Client
# Query public schema
from django_tenants.utils import schema_context
with schema_context('public'):
tenants = Client.objects.exclude(schema_name='public')
for tenant in tenants:
print(f"{tenant.name}: {tenant.schema_name}")
for domain in tenant.domains.all():
print(f" - {domain.domain}")
# Delete tenant
def delete_tenant(schema_name):
from customers.models import Client
from django_tenants.utils import schema_context
with schema_context('public'):
tenant = Client.objects.get(schema_name=schema_name)
tenant.delete() # Automatically drops schema
# Working with tenant data
from django_tenants.utils import tenant_context
from customers.models import Client
from myapp.models import Project
def get_tenant_projects(tenant_schema):
tenant = Client.objects.get(schema_name=tenant_schema)
with tenant_context(tenant):
projects = Project.objects.all()
return projects
# Execute code for all tenants
from customers.models import Client
def update_all_tenants():
tenants = Client.objects.exclude(schema_name='public')
for tenant in tenants:
with tenant_context(tenant):
# Do something in each tenant's schema
Project.objects.filter(archived=True).delete()
print(f"Cleaned up {tenant.name}")Subdomain Routing and Tenant Detection
Subdomain routing identifies tenants through URLs like tenant1.example.com and tenant2.example.com with middleware automatically switching to appropriate schema. Public URLs handle landing pages and signup while tenant subdomains serve application features. Understanding routing configuration enables proper tenant isolation maintaining seamless user experience across tenant boundaries.
# URL Configuration
# Public URLs (shared schema)
# myproject/urls_public.py
from django.contrib import admin
from django.urls import path
from customers import views as customer_views
urlpatterns = [
path('', customer_views.landing_page, name='landing'),
path('signup/', customer_views.tenant_signup, name='signup'),
path('pricing/', customer_views.pricing, name='pricing'),
path('admin/', admin.site.urls),
]
# Tenant URLs (tenant-specific schemas)
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from myapp import views
urlpatterns = [
path('', views.dashboard, name='dashboard'),
path('projects/', include('myapp.urls')),
path('admin/', admin.site.urls),
]
# Local development with subdomains
# Add to /etc/hosts (Linux/Mac) or C:\Windows\System32\drivers\etc\hosts
127.0.0.1 example.local
127.0.0.1 tenant1.example.local
127.0.0.1 tenant2.example.local
# Access URLs:
# http://example.local:8000 (public)
# http://tenant1.example.local:8000 (tenant1)
# http://tenant2.example.local:8000 (tenant2)
# Custom tenant middleware for debugging
# myapp/middleware.py
from django_tenants.utils import get_tenant_model
class TenantDebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Log current tenant
if hasattr(request, 'tenant'):
print(f"Current tenant: {request.tenant.name}")
print(f"Schema: {request.tenant.schema_name}")
response = self.get_response(request)
return response
# Tenant-aware views
# myapp/views.py
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import Project
@login_required
def dashboard(request):
# Automatically queries current tenant's schema
projects = Project.objects.filter(owner=request.user)
context = {
'tenant': request.tenant,
'projects': projects,
}
return render(request, 'dashboard.html', context)
@login_required
def project_list(request):
# All queries are tenant-scoped automatically
projects = Project.objects.all()
return render(request, 'projects/list.html', {'projects': projects})
# Production Nginx configuration
# /etc/nginx/sites-available/saas
server {
listen 80;
server_name *.example.com example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /var/www/saas/staticfiles/;
}
}Shared vs Tenant-Specific Models
Shared models exist in public schema storing tenant metadata, billing information, and global settings while tenant-specific models exist per schema containing customer data. Properly segregating models maintains efficiency and security. Understanding model organization integrated with Django relationships enables building scalable multi-tenant architectures.
# Shared models (public schema)
# customers/models.py
from django.db import models
from django_tenants.models import TenantMixin, DomainMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
created_on = models.DateField(auto_now_add=True)
on_trial = models.BooleanField(default=True)
trial_ends = models.DateField(null=True, blank=True)
# Subscription info
plan = models.CharField(
max_length=50,
choices=[
('free', 'Free'),
('pro', 'Pro'),
('enterprise', 'Enterprise')
],
default='free'
)
max_users = models.IntegerField(default=5)
max_projects = models.IntegerField(default=10)
auto_create_schema = True
class Domain(DomainMixin):
pass
class BillingInfo(models.Model):
"""Stored in public schema"""
tenant = models.OneToOneField(Client, on_delete=models.CASCADE)
stripe_customer_id = models.CharField(max_length=100)
last_payment = models.DateField(null=True)
next_billing_date = models.DateField()
# Tenant-specific models (per-tenant schemas)
# myapp/models.py
from django.db import models
from django.contrib.auth.models import User
class Project(models.Model):
"""Stored per tenant"""
name = models.CharField(max_length=200)
description = models.TextField()
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Task(models.Model):
"""Stored per tenant"""
project = models.ForeignKey(Project, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
completed = models.BooleanField(default=False)
# Custom tenant manager
class TenantAwareManager(models.Manager):
def for_current_tenant(self):
from django_tenants.utils import get_current_tenant
tenant = get_current_tenant()
# Custom filtering logic
return self.filter(some_field=tenant.id)Multi-Tenancy Best Practices
- Test tenant isolation: Write tests ensuring tenants cannot access other tenants' data maintaining security guarantees
- Use schema-based isolation: Prefer PostgreSQL schemas over shared tables with TenantID providing stronger isolation
- Implement proper migrations: Use migrate_schemas command running migrations across all tenant schemas
- Monitor schema count: PostgreSQL handles hundreds of schemas but consider alternative strategies beyond thousands
- Cache tenant lookups: Use caching for tenant resolution avoiding repeated database queries
- Implement billing integration: Track tenant usage and subscription status enforcing plan limits automatically
- Plan for tenant deletion: Implement data export and schema cleanup handling customer churn properly
- Use connection pooling: Configure proper database connection pooling handling multiple schema connections efficiently
- Implement tenant provisioning: Automate tenant creation with email verification and onboarding workflows
- Monitor per-tenant metrics: Track usage, performance, and costs per tenant identifying optimization opportunities
Conclusion
Multi-tenancy enables single Django application serving multiple customers with complete data isolation crucial for SaaS platforms where numerous organizations share infrastructure reducing deployment costs dramatically compared to single-tenant architectures requiring separate instances per customer. Schema-based isolation using django-tenants creates separate PostgreSQL schemas per tenant providing strong data separation while sharing application code and database connections optimizing resource utilization. Implementation requires configuring django-tenants with TenantModel and DomainModel defining tenant metadata stored in public schema while tenant-specific data resides in per-tenant schemas automatically created during tenant provisioning. Subdomain routing identifies tenants through URLs like tenant.example.com with middleware automatically switching to appropriate schema enabling seamless multi-tenant experience where users cannot access other tenants' data through application interface. Creating tenants involves generating Client and Domain instances with django-tenants automatically creating schemas and running migrations for tenant-specific tables while management commands automate tenant provisioning enabling self-service signup flows. Shared models exist in public schema storing tenant metadata, billing information, and global settings while tenant-specific models contain customer data isolated per schema maintaining security and compliance requirements. URL configuration separates public URLs for landing pages and signup from tenant URLs serving application features with different URL patterns handling different functionality maintaining clean separation. Best practices include testing tenant isolation thoroughly ensuring security, using schema-based isolation providing stronger guarantees than shared tables, implementing proper migration workflows across all schemas, monitoring schema count for scalability limits, caching tenant lookups avoiding repeated queries, integrating billing tracking usage and enforcing limits, planning tenant deletion with data export, configuring connection pooling efficiently, automating tenant provisioning with onboarding, and monitoring per-tenant metrics identifying optimization opportunities. Understanding multi-tenancy architecture from schema isolation through tenant management integrated with Django models, authentication, routing, and deployment strategies enables building production-ready SaaS applications serving thousands of tenant organizations from single deployment maintaining security, performance, and cost efficiency throughout application lifecycle from initial tenant through enterprise scale.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


