Multi-Tenant Applications
Build secure multi-tenant applications where data is completely isolated between tenants.
The Multi-Tenant Challenge
In multi-tenant applications, ensuring complete data isolation is critical:
# Without RLS - Dangerous if you forget to filter!
def product_list(request):
# WRONG: Could expose other tenants' data
products = Product.objects.all()
# RIGHT: Must always remember tenant filtering
products = Product.objects.filter(tenant=request.user.tenant)
Complete Multi-Tenant Solution
1. Tenant Model
from django.db import models
class Tenant(models.Model):
name = models.CharField(max_length=100)
subdomain = models.SlugField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
2. User-Tenant Relationship
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
related_name='users'
)
3. Tenant-Aware Models
from django_rls.models import RLSModel
from django_rls.policies import TenantPolicy
class Customer(RLSModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
email = models.EmailField()
class Meta:
rls_policies = [
TenantPolicy('tenant_isolation', tenant_field='tenant'),
]
class Product(RLSModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
rls_policies = [
TenantPolicy('tenant_isolation', tenant_field='tenant'),
]
class Order(RLSModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
total = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
rls_policies = [
TenantPolicy('tenant_isolation', tenant_field='tenant'),
]
4. Tenant Detection Middleware
from django_rls.middleware import RLSContextMiddleware
class TenantMiddleware:
"""Detect tenant from subdomain."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Extract subdomain
host = request.get_host().split(':')[0]
subdomain = host.split('.')[0]
try:
request.tenant = Tenant.objects.get(
subdomain=subdomain,
is_active=True
)
except Tenant.DoesNotExist:
request.tenant = None
return self.get_response(request)
# In settings.py
MIDDLEWARE = [
# ... other middleware
'myapp.middleware.TenantMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_rls.middleware.RLSContextMiddleware',
]
5. Views Are Simple
@login_required
def customer_list(request):
# Automatically filtered to current tenant!
customers = Customer.objects.all()
return render(request, 'customers/list.html', {
'customers': customers,
'tenant': request.tenant,
})
@login_required
def dashboard(request):
# All queries respect tenant isolation
context = {
'customer_count': Customer.objects.count(),
'product_count': Product.objects.count(),
'recent_orders': Order.objects.order_by('-created_at')[:10],
'total_revenue': Order.objects.aggregate(Sum('total')),
}
return render(request, 'dashboard.html', context)
Advanced Multi-Tenant Patterns
Tenant-Specific Settings
class TenantSettings(RLSModel):
tenant = models.OneToOneField(Tenant, on_delete=models.CASCADE)
theme_color = models.CharField(max_length=7, default='#007bff')
logo = models.ImageField(upload_to='tenant_logos/', null=True)
features = models.JSONField(default=dict)
class Meta:
rls_policies = [
TenantPolicy('tenant_isolation', tenant_field='tenant'),
]
Cross-Tenant Data Sharing
Some data might be shared across tenants:
class GlobalProduct(RLSModel):
"""Products available to all tenants."""
name = models.CharField(max_length=200)
base_price = models.DecimalField(max_digits=10, decimal_places=2)
# No RLS policies - accessible to all
class TenantProduct(RLSModel):
"""Tenant-specific product customization."""
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
global_product = models.ForeignKey(GlobalProduct, on_delete=models.CASCADE)
custom_price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
is_available = models.BooleanField(default=True)
class Meta:
rls_policies = [
TenantPolicy('tenant_isolation', tenant_field='tenant'),
]
unique_together = [['tenant', 'global_product']]
Tenant Admin Users
Allow admin users to access multiple tenants:
class TenantUser(RLSModel):
"""User access to specific tenants."""
user = models.ForeignKey(User, on_delete=models.CASCADE)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=[
('admin', 'Admin'),
('member', 'Member'),
('viewer', 'Viewer'),
])
class Meta:
rls_policies = [
# Users can see their own tenant memberships
UserPolicy('user_access', user_field='user'),
]
class MultiTenantModel(RLSModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
# ... other fields
class Meta:
rls_policies = [
CustomPolicy(
'multi_tenant_access',
expression="""
tenant_id IN (
SELECT tenant_id FROM myapp_tenantuser
WHERE user_id = current_setting('rls.user_id')::integer
)
"""
),
]
Testing Multi-Tenant Applications
from django.test import TestCase
from django_rls.test import RLSTestMixin
class TenantIsolationTest(RLSTestMixin, TestCase):
def setUp(self):
# Create tenants
self.tenant1 = Tenant.objects.create(
name='Acme Corp',
subdomain='acme'
)
self.tenant2 = Tenant.objects.create(
name='Globex Inc',
subdomain='globex'
)
# Create users
self.user1 = User.objects.create_user(
username='john',
tenant=self.tenant1
)
self.user2 = User.objects.create_user(
username='jane',
tenant=self.tenant2
)
def test_tenant_isolation(self):
# Create data for tenant1
with self.with_context(user_id=self.user1.id,
tenant_id=self.tenant1.id):
Customer.objects.create(
tenant=self.tenant1,
name='Acme Customer'
)
# Verify tenant2 cannot see it
with self.with_context(user_id=self.user2.id,
tenant_id=self.tenant2.id):
customers = Customer.objects.all()
self.assertEqual(customers.count(), 0)
Performance Optimization
Indexes for Tenant Fields
class TenantModel(RLSModel):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_index=True)
# ... other fields
class Meta:
indexes = [
models.Index(fields=['tenant', '-created_at']),
]
Tenant-Specific Database Connections
For large-scale applications:
class TenantDatabaseRouter:
"""Route queries to tenant-specific databases."""
def db_for_read(self, model, **hints):
if hasattr(model, '_meta') and hasattr(model._meta, 'rls_policies'):
# Get current tenant from thread-local storage
tenant_id = getattr(_thread_locals, 'tenant_id', None)
if tenant_id:
return f'tenant_{tenant_id}'
return 'default'
Best Practices
- Always include tenant field in RLS-enabled models
- Use database constraints to ensure referential integrity
- Index tenant fields for performance
- Test tenant isolation thoroughly
- Monitor query performance per tenant
- Plan for tenant data migration and archival