Django’s multi-tenancy with separate schemas isn’t just about isolating data; it’s about enforcing isolation at the database level, giving each tenant their own schema to prevent cross-tenant data leaks and simplify management.
Let’s see this in action. Imagine a Tenant model and a Client model.
# models.py
from django.db import models
class Tenant(models.Model):
name = models.CharField(max_length=100, unique=True)
schema_name = models.CharField(max_length=63, unique=True) # Database schema name
def __str__(self):
return self.name
class Client(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
email = models.EmailField()
def __str__(self):
return f"{self.name} ({self.tenant.name})"
When a new tenant is created, we also create a new PostgreSQL schema for them.
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import connection
from .models import Tenant
@receiver(post_save, sender=Tenant)
def create_tenant_schema(sender, instance, created, **kwargs):
if created:
with connection.cursor() as cursor:
cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {instance.schema_name};")
# You'd typically run migrations here to set up the schema's tables
# For simplicity, we're just creating the schema.
# In a real app, use `django-tenants` or similar for full migration management.
Now, how do we ensure requests hit the right schema? We use middleware.
# middleware.py
from django.db import connection
from .models import Tenant
class TenantSchemaMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tenant_slug = request.META.get('HTTP_X_TENANT_SLUG') # Assumes a custom header
if tenant_slug:
try:
tenant = Tenant.objects.get(schema_name=tenant_slug)
connection.set_schema(tenant.schema_name)
except Tenant.DoesNotExist:
# Handle invalid tenant, perhaps return 404 or 403
pass # For now, let it fall through
response = self.get_response(request)
connection.set_schema(None) # Reset schema after request
return response
The problem this solves is scaling your Django application to handle many distinct "customers" or "organizations" while keeping their data completely separate. Instead of a single, massive clients table with a tenant_id column, you have a clients table within each tenant’s schema. This means that SELECT * FROM clients in one tenant’s schema will never show data from another tenant’s schema.
Internally, PostgreSQL schemas are just namespaces. When you SET search_path TO my_tenant_schema, public;, any unqualified table references (like Client) will first look for my_tenant_schema.Client. Django’s database connection object lets you set_schema to dynamically change this search_path.
The levers you control are:
- Tenant Identification: How do you know which tenant is making the request? Common methods include subdomain (
tenant1.yourapp.com), a custom HTTP header (X-Tenant-Slug: tenant1), or a URL path (yourapp.com/tenant1/). - Schema Creation/Management: When a new tenant signs up, you need to create their schema, potentially run initial migrations on it, and set up any necessary defaults. Libraries like
django-tenantsautomate much of this. - Request Routing: Middleware is key to switching the database connection’s active schema based on the identified tenant for the duration of the request.
- Tenant-Agnostic Code: Your Django models and views should ideally not need to know about
tenant_idcolumns. They just queryClientand get the right data because the schema is already set.
The one thing most people don’t realize is that your "shared" apps, like Django’s built-in auth or admin, often live in the public schema. When you switch schemas, you might temporarily lose access to these unless you explicitly include public in your search_path for every request. A common pattern is SET search_path TO <tenant_schema>, public;, ensuring that both tenant-specific tables and shared system tables are accessible.
The next hurdle is managing tenant-specific static files or media, which often requires a different approach than database schemas.