$ cat /posts/django-multi-tenancy-building-saas-applications.md

Django Multi-Tenancy: Building SaaS Applications

drwxr-xr-x2026-01-245 min0 views
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.

StrategyIsolation LevelScalabilityCostBest For
Separate DatabasesComplete isolationLimited by connectionsHighFew large enterprise tenants
Separate SchemasStrong isolationGood (100s tenants)MediumMost SaaS applications
Shared Schema + FilterApplication-levelExcellent (1000s tenants)LowHigh-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.

pythonmulti_tenancy_setup.py
# 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 --tenant

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

pythontenant_management.py
# 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.

pythontenant_routing.py
# 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.

pythontenant_models.py
# 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)
Always test tenant isolation thoroughly ensuring tenants cannot access other tenants' data. Use automated tests querying across different tenant contexts verifying complete data separation maintaining security compliance requirements for SaaS applications handling sensitive customer data.

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
Multi-tenancy enables efficient SaaS applications serving thousands of customers from single deployment. Django-tenants provides schema-based isolation maintaining security while optimizing resources integrated with Django security best practices throughout application lifecycle.

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.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.