A comprehensive multi-tenancy solution for Django that simplifies building scalable SaaS applications with flexible tenant isolation strategies.
django-omnitenant is a production-ready Django application for implementing multi-tenancy in your Django projects. It provides out-of-the-box support for multiple tenant isolation strategies, automatic tenant context management, and comprehensive management commands for tenant lifecycle operations.
- Multiple Isolation Strategies: Support for database-per-tenant and schema-per-tenant isolation
- Transparent Tenant Context: Thread-safe tenant context management using Python's
contextvars - Production-Ready: Battle-tested patterns for multi-tenant Django applications
- Extensible Design: Pluggable resolvers, backends, and patch system
- Developer Friendly: Comprehensive CLI tools and intuitive API
- Signal Integration: Django signals at key tenant lifecycle events
-
Multi-Tenant Isolation: Choose between:
- Database-per-Tenant: Each tenant isolated in a separate database
- Schema-per-Tenant: All tenants share a database but use separate PostgreSQL schemas
-
Tenant Resolution: Multiple strategies for identifying the current tenant:
- Custom domain resolver
- Subdomain resolver
- Extensible resolver interface for custom implementations
-
Automatic Context Management: Thread-safe tenant context with context managers for seamless tenant switching
-
Tenant-Aware Components:
- Database routing with automatic query isolation
- Cache backend with tenant-scoped keys
- Celery task integration for tenant-aware background jobs
- Admin interface restrictions for tenant data isolation
Comprehensive CLI for tenant operations:
createtenant- Create new tenants interactivelycreatetenantsuperuser- Create admin users per tenantmigratetenant- Run migrations for specific tenantsmigratealltenants- Batch migrate all tenantsshell- Django shell with tenant contextshowtenants- List and export tenant information
- DNS label validation for tenant identifiers
- Domain name validation for custom domains
- Admin access restrictions to tenant-specific models
- Tenant data isolation at the database/schema level
- Python 3.8+
- Django 3.2+
- PostgreSQL 10+ (recommended for schema isolation)
- psycopg2-binary (for PostgreSQL support)
Optional dependencies:
- Celery (for async task support)
- Redis (for distributed caching)
pip install django-omnitenantgit clone https://github.com/RahulRimal/django-omnitenant.git
cd django-omnitenant
pip install -e .# settings.py
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.admin',
# ...
'myapp', # Your application
'django_omnitenant',
]# settings.py
OMNITENANT_CONFIG = {
'TENANT_MODEL': 'myapp.Tenant',
'DOMAIN_MODEL': 'myapp.Domain',
'PUBLIC_HOST': 'example.com',
'PUBLIC_TENANT_NAME': 'public',
'MASTER_TENANT_NAME': 'master',
'TENANT_RESOLVER': 'django_omnitenant.resolvers.CustomDomainTenantResolver',
}# myapp/models.py
from django.db import models
from django_omnitenant.models import BaseTenant, BaseDomain
class Tenant(BaseTenant):
"""Custom tenant model extending BaseTenant."""
description = models.TextField(blank=True)
def __str__(self):
return self.name
class Domain(BaseDomain):
"""Custom domain model linking domains to tenants."""
pass# settings.py
MIDDLEWARE = [
# ... other middleware
'django_omnitenant.middleware.TenantMiddleware',
# ... other middleware
]# settings.py
DATABASE_ROUTERS = [
'django_omnitenant.routers.TenantRouter',
]python manage.py migratepython manage.py createtenant
# Follow the interactive prompts to create a tenantpython manage.py createtenantThe command will prompt for:
- Tenant ID (unique identifier)
- Tenant Name (display name)
- Isolation Type (database or schema)
- Database credentials (for database isolation)
from django_omnitenant.utils import get_tenant_model, get_tenant_backend
Tenant = get_tenant_model()
# Create tenant
tenant = Tenant.objects.create(
tenant_id='acme',
name='ACME Corporation',
isolation_type=Tenant.IsolationType.DATABASE, # or SCHEMA
config={
'db_config': {
'NAME': 'acme_db',
'USER': 'acme_user',
'PASSWORD': 'secure_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
)
# Provision resources and run migrations
backend = get_tenant_backend(tenant)
backend.create(run_migrations=True)from django.http import JsonResponse
def my_view(request):
# Tenant is automatically resolved and set by middleware
tenant = request.tenant
return JsonResponse({'tenant': tenant.tenant_id})Use the tenant-aware manager:
from django_omnitenant.models import TenantQuerySetManager
class MyModel(models.Model):
name = models.CharField(max_length=255)
objects = TenantQuerySetManager()
class Meta:
# Queries are automatically scoped to current tenant
passQuery within tenant context:
from django_omnitenant.tenant_context import TenantContext
with TenantContext.use_tenant(tenant):
items = MyModel.objects.all() # Queries tenant's database/schema# Migrate specific tenant
python manage.py migratetenant --tenant-id=acme
# Migrate all tenants
python manage.py migratealltenants
# Show migration plan
python manage.py migratetenant --tenant-id=acme --planpython manage.py createtenantsuperuser --tenant-id=acme# List all tenants
python manage.py showtenants
# Export as JSON
python manage.py showtenants --format=json
# Export as CSV
python manage.py showtenants --format=csv
# Filter by isolation type
python manage.py showtenants --isolation-type=database# Shell with tenant context
python manage.py shell --tenant-id=acme
# Inside the shell
>>> from myapp.models import MyModel
>>> MyModel.objects.all() # Queries only acme's dataCreate a custom resolver for specialized tenant resolution logic:
# myapp/resolvers.py
from django_omnitenant.resolvers.base import BaseTenantResolver
from django_omnitenant.exceptions import TenantNotFound
class HeaderTenantResolver(BaseTenantResolver):
"""Resolve tenant from HTTP header."""
def resolve(self, request):
"""Extract tenant from X-Tenant-ID header."""
from django_omnitenant.utils import get_tenant_model
tenant_id = request.headers.get('X-Tenant-ID')
if not tenant_id:
raise TenantNotFound("No X-Tenant-ID header provided")
Tenant = get_tenant_model()
try:
return Tenant.objects.get(tenant_id=tenant_id)
except Tenant.DoesNotExist:
raise TenantNotFound(f"Tenant '{tenant_id}' not found")Configure in settings:
# settings.py
OMNITENANT_CONFIG = {
'TENANT_RESOLVER': 'myapp.resolvers.HeaderTenantResolver',
}Restrict admin access to tenant-specific data:
# myapp/admin.py
from django.contrib import admin
from django_omnitenant.admin import TenantRestrictAdminMixin
from .models import MyModel
@admin.register(MyModel)
class MyModelAdmin(TenantRestrictAdminMixin, admin.ModelAdmin):
list_display = ['name', 'created_at']
# Admin access restricted to master tenant only# myapp/tasks.py
from celery import shared_task
from django_omnitenant.tenant_context import TenantContext
@shared_task
def process_tenant_data(tenant_id):
"""Process data for specific tenant."""
from django_omnitenant.utils import get_tenant_model
Tenant = get_tenant_model()
tenant = Tenant.objects.get(tenant_id=tenant_id)
with TenantContext.use_tenant(tenant):
# Task runs in tenant context
passCall from your code:
process_tenant_data.delay(tenant_id='acme')Switch tenant context for specific operations:
from django_omnitenant.tenant_context import TenantContext
from django_omnitenant.utils import get_tenant_model
Tenant = get_tenant_model()
acme_tenant = Tenant.objects.get(tenant_id='acme')
# Temporarily switch to tenant
with TenantContext.use_tenant(acme_tenant):
# All queries here use acme's database/schema
items = MyModel.objects.all()
# Back to original context
# Switch to master database
with TenantContext.use_master_db():
# Access master database
pass
# Switch to specific schema
with TenantContext.use_schema('tenant_acme'):
# Access specific schema
passOMNITENANT_CONFIG = {
# Database and schema settings
'TENANT_MODEL': 'myapp.Tenant', # Path to tenant model
'DOMAIN_MODEL': 'myapp.Domain', # Path to domain model
# Request resolution
'PUBLIC_HOST': 'example.com', # Default public host
'TENANT_RESOLVER': 'django_omnitenant.resolvers.CustomDomainTenantResolver',
# Tenant identification
'PUBLIC_TENANT_NAME': 'public', # Public/shared tenant
'MASTER_TENANT_NAME': 'master', # Master tenant (shared data)
# Optional patches
'PATCHES': [
'django_omnitenant.patches.cache',
'django_omnitenant.patches.celery',
],
}# Database aliases
MASTER_DB_ALIAS = 'default' # Master database alias
PUBLIC_DB_ALIAS = 'default' # Public database alias
MASTER_CACHE_ALIAS = 'default' # Master cache alias
# Schema settings
DEFAULT_SCHEMA_NAME = 'public' # Default PostgreSQL schemadjango_omnitenant.resolvers.CustomDomainTenantResolver- Resolve from custom domaindjango_omnitenant.resolvers.SubdomainTenantResolver- Resolve from subdomain
Each tenant has a dedicated database:
Master Database Tenant Databases
┌─────────────────┐ ┌──────────────┐
│ Tenants │ │ tenant_acme │
│ Domains │ └──────────────┘
│ Shared Config │ ┌──────────────┐
└─────────────────┘ │ tenant_globex│
└──────────────┘
Pros: Complete isolation, independent scaling Cons: More infrastructure, connection overhead
All tenants share a database with separate schemas:
Single Database
┌──────────────────────────────┐
│ public schema │
│ ├─ tenants │
│ ├─ domains │
│ └─ shared tables │
│ │
│ tenant_acme schema │
│ ├─ users │
│ ├─ products │
│ └─ orders │
│ │
│ tenant_globex schema │
│ ├─ users │
│ ├─ products │
│ └─ orders │
└──────────────────────────────┘
Pros: Shared infrastructure, simpler management, faster schema creation Cons: Less isolation, shared connection pool
- Middleware: Resolves tenant from HTTP request
- Router: Routes queries to correct database/schema
- Context: Thread-safe tenant context using
contextvars - Backends: Handle provisioning (database/schema creation)
- Resolvers: Extract tenant from request using various strategies
- Signals: Emit events at tenant lifecycle stages
- Management Commands: CLI tools for tenant operations
Use provided test case mixins:
from django_omnitenant.tests.testcases import (
BaseTenantTestCase,
DBTenantTestCase,
SchemaTenantTestCase,
)
class MyTestCase(DBTenantTestCase):
"""Tests for database-per-tenant isolation."""
def test_tenant_isolation(self):
# Test case automatically sets up tenant context
from myapp.models import MyModel
# Create data in tenant
MyModel.objects.create(name='Test')
# Verify isolation
assert MyModel.objects.count() == 1TenantContext- Manage tenant context- Methods:
get_tenant(),use_tenant(),use_master_db(),use_schema()
BaseTenant- Abstract tenant modelBaseDomain- Abstract domain modelTenantQuerySetManager- Tenant-aware query manager
BaseTenantBackend- Abstract backendDatabaseTenantBackend- Database isolationSchemaTenantBackend- Schema isolationCacheTenantBackend- Cache management
get_tenant_model()- Get configured tenant modelget_domain_model()- Get configured domain modelget_tenant_backend()- Get backend for tenantget_current_tenant()- Get current tenant
TenantNotFound- Tenant resolution failedDomainNotFound- Domain resolution failed
See full API documentation for complete reference.
from rest_framework import serializers
from django_omnitenant.tenant_context import TenantContext
class TenantAwareSerializer(serializers.ModelSerializer):
def validate(self, data):
# Validations run in current tenant context
return datafrom django_omnitenant.tenant_context import TenantContext
for tenant in Tenant.objects.using('default').all():
with TenantContext.use_tenant(tenant):
# Query in each tenant's context
count = MyModel.objects.count()
print(f"{tenant.name}: {count} items")from django_omnitenant.signals import tenant_created, tenant_migrated
@receiver(tenant_created)
def setup_tenant(sender, tenant, **kwargs):
"""Run custom setup after tenant creation."""
pass
@receiver(tenant_migrated)
def post_migration_setup(sender, tenant, **kwargs):
"""Run custom setup after migrations."""
pass- Connection Pooling: Use
CONN_MAX_AGEin DATABASES settings - Query Optimization: Add indexes per tenant database/schema
- Caching Strategy: Use tenant-scoped cache keys (automatic)
- Signal Handlers: Keep signal handlers lightweight
- Bulk Operations: Use
bulk_create()andbulk_update()in tenant context
- Validate Tenant Access: Always verify tenant context in views
- Secure Credentials: Store DB credentials securely (env variables, vaults)
- Audit Logging: Log cross-tenant operations
- Rate Limiting: Implement per-tenant rate limiting
- Data Isolation: Verify isolation with security tests
- Admin Access: Restrict admin to master tenant
- Signal Security: Validate tenant context in signal handlers
# Error: TenantNotFound
# Solution: Verify TENANT_RESOLVER configuration and domain mapping# Error: Data queried from wrong tenant
# Solution: Ensure TenantMiddleware is in MIDDLEWARE
# or explicitly use TenantContext.use_tenant()# Error: Migration fails for specific tenant
# Solution: Run with --no-input flag
python manage.py migratetenant --tenant-id=acme --no-inputSee Troubleshooting Guide for more.
# Clone repository
git clone https://github.com/RahulRimal/django-omnitenant.git
cd django-omnitenant
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run with coverage
pytest --cov=django_omnitenant- Follow PEP 8
- Use Black for formatting
- Use isort for import sorting
- Type hints required for public APIs
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CONTRIBUTING.md for details.
See CHANGELOG.md for release notes and breaking changes.
- Hybrid isolation strategy (database + schema)
- REST API for tenant management
- Tenant analytics dashboard
- Performance monitoring tools
- Multi-database support (MySQL, Oracle)
This project is licensed under the MIT License - see LICENSE file for details.
- Documentation: Read the Docs
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Please include:
- Django version
- Python version
- Isolation strategy used
- Minimal reproducible example
- Error traceback
If you use django-omnitenant in your research, please cite:
@software{rimal2024django-omnitenant,
author = {Krishna Rimal},
title = {django-omnitenant: Multi-tenancy for Django},
url = {https://github.com/RahulRimal/django-omnitenant},
year = {2024},
}- Inspired by django-tenant-schemas
- Built for modern Django applications
- Special thanks to the Django community
Made with ❤️ by Ajna Lab for the Django community