How to Build a Multi-Tenant Application with Django
A multi-tenant application serves multiple customers (tenants) from a single instance of the application, with each tenant’s data isolated from the others. This approach benefits SaaS (Software as a Service) applications where multiple organizations or users need separate environments.
Here’s a detailed guide on how to build a multi-tenant application with Django:
1. Introduction to Multi-Tenancy
Types of Multi-Tenancy:
- Database-per-tenant: Each tenant has its database.
- Schema-per-tenant: All tenants share the same database but have separate schemas.
- Shared-database, Shared-schema: All tenants share the same database and schema, with rows separated by a tenant identifier.
This guide will focus on the shared-database, shared-schema approach, which Django commonly uses and supports.
2. Setting Up the Project
Install Django and Create a New Project:
pip install django
django-admin startproject multitenant
cd multitenant
Create a New Django App:
python manage.py startapp core
3. Defining the Tenant Model
Tenant Model: This model will hold data about each tenant.
# core/models.py
from django.db import models
class Tenant(models.Model):
name = models.CharField(max_length=100)
domain = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
Setting Up Tenant-Specific Models: Use a foreign key to associate models with tenants.
# core/models.py
from django.db import models
class Tenant(models.Model):
name = models.CharField(max_length=100)
domain = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
4. Middleware for Tenant Identification
Create middleware to identify the tenant based on the request.
Middleware to Detect Tenant:
# core/middleware.py
from django.utils.deprecation import MiddlewareMixin
from core.models import Tenant
class TenantMiddleware(MiddlewareMixin):
def process_request(self, request):
domain = request.get_host().split(':')[0]
try:
request.tenant = Tenant.objects.get(domain=domain)
except Tenant.DoesNotExist:
request.tenant = None
Register Middleware:
# multitenant/settings.py
MIDDLEWARE = [
...
'core.middleware.TenantMiddleware',
...
]
5. Query Filtering by Tenant
Ensure that the tenant filters queries.
Manager for Tenant Filtering:
# core/models.py
from django.db import models
from django.conf import settings
class TenantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(tenant=get_current_tenant())
def get_current_tenant():
from threading import local
_local = local()
return getattr(_local, 'tenant', None)
class Customer(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
email = models.EmailField()
objects = TenantManager()
def __str__(self):
return self.name
Setting the Current Tenant:
Update the middleware to set the current tenant globally.
# core/middleware.py
from threading import local
_local = local()
class TenantMiddleware(MiddlewareMixin):
def process_request(self, request):
domain = request.get_host().split(':')[0]
try:
request.tenant = Tenant.objects.get(domain=domain)
_local.tenant = request.tenant
except Tenant.DoesNotExist:
request.tenant = None
_local.tenant = None
return self.name
6. Admin and Views Adjustments
Ensure that Django Admin and views respect tenant boundaries.
Admin Customization:
# core/admin.py
from django.contrib import admin
from .models import Customer, Tenant
class TenantAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.tenant:
return qs.filter(tenant=request.tenant)
return qs
admin.site.register(Customer, TenantAdmin)
admin.site.register(Tenant)
Views and Forms:
Adjust views and forms to filter by tenant automatically.
# core/views.py
from django.shortcuts import render
from .models import Customer
def customer_list(request):
customers = Customer.objects.filter(tenant=request.tenant)
return render(request, 'core/customer_list.html', {'customers': customers})
7. Security and Testing
Security Considerations:
- Isolate tenant data: Ensure strict data separation.
- Authentication and Authorization: Implement proper checks to prevent unauthorized access.
Testing Multi-Tenancy:
Create tests to verify that data access is properly restricted based on the tenant.
# core/tests.py
from django.test import TestCase
from .models import Tenant, Customer
class TenantTestCase(TestCase):
def setUp(self):
self.tenant1 = Tenant.objects.create(name='Tenant 1', domain='tenant1.example.com')
self.tenant2 = Tenant.objects.create(name='Tenant 2', domain='tenant2.example.com')
Customer.objects.create(tenant=self.tenant1, name='Customer 1', email='customer1@example.com')
Customer.objects.create(tenant=self.tenant2, name='Customer 2', email='customer2@example.com')
def test_tenant_customers(self):
self.client.get('/', HTTP_HOST='tenant1.example.com')
customers = Customer.objects.all()
self.assertEqual(customers.count(), 1)
self.assertEqual(customers[0].name, 'Customer 1')
self.client.get('/', HTTP_HOST='tenant2.example.com')
customers = Customer.objects.all()
self.assertEqual(customers.count(), 1)
self.assertEqual(customers[0].name, 'Customer 2')
This approach sets up a basic multi-tenant architecture in Django, ensuring that each tenant’s data is isolated while sharing the same database and schema. This solution is scalable, efficient, and maintains data integrity across tenants.
To read more about enhancing application security with Django AuditLog, refer to our blog How to Enhance Application Security with Django AuditLog