A Django app to allow multi-tenancy.
Project description
django-omnitenant
A comprehensive multi-tenancy solution for Django that simplifies building scalable SaaS applications with flexible tenant isolation strategies.
Overview
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.
Why django-omnitenant?
- 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
Features
Core Functionality
-
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
Management Commands
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
Security & Validation
- 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
Requirements
- 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)
Installation
Via pip
pip install django-omnitenant
From source
git clone https://github.com/RahulRimal/django-omnitenant.git
cd django-omnitenant
pip install -e .
Quick Start
1. Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.admin',
# ...
'django_omnitenant',
'myapp', # Your application
]
2. Configure 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',
}
3. Create Tenant Models
# 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
4. Add Middleware
# settings.py
MIDDLEWARE = [
# ... other middleware
'django_omnitenant.middleware.TenantMiddleware',
# ... other middleware
]
5. Configure Database Router
# settings.py
DATABASE_ROUTERS = [
'django_omnitenant.routers.TenantRouter',
]
6. Run Migrations
python manage.py migrate
7. Create Your First Tenant
python manage.py createtenant
# Follow the interactive prompts to create a tenant
Usage Guide
Creating Tenants
Interactive Mode
python manage.py createtenant
The command will prompt for:
- Tenant ID (unique identifier)
- Tenant Name (display name)
- Isolation Type (database or schema)
- Database credentials (for database isolation)
Programmatic Creation
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)
Accessing Tenant Data
In Views
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})
In Models
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
pass
Query within tenant context:
from django_omnitenant.tenant_context import TenantContext
with TenantContext.use_tenant(tenant):
items = MyModel.objects.all() # Queries tenant's database/schema
Managing Tenant Lifecycle
Running Migrations
# 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 --plan
Creating Superusers
python manage.py createtenantsuperuser --tenant-id=acme
Viewing Tenants
# 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
Interactive Shell
# 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 data
Advanced: Custom Tenant Resolver
Create 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',
}
Advanced: Tenant-Aware Admin
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
Background Tasks with Celery
# 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
pass
Call from your code:
process_tenant_data.delay(tenant_id='acme')
Context Managers
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
pass
Configuration Reference
Required Settings
OMNITENANT_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',
],
}
Optional Settings
# 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 schema
Built-in Resolvers
django_omnitenant.resolvers.CustomDomainTenantResolver- Resolve from custom domaindjango_omnitenant.resolvers.SubdomainTenantResolver- Resolve from subdomain
Architecture
Isolation Strategies
Database-per-Tenant
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
Schema-per-Tenant
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
Component Overview
- 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
Testing
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() == 1
API Reference
Key Classes and Functions
Tenant Context
TenantContext- Manage tenant context- Methods:
get_tenant(),use_tenant(),use_master_db(),use_schema()
Models
BaseTenant- Abstract tenant modelBaseDomain- Abstract domain modelTenantQuerySetManager- Tenant-aware query manager
Backends
BaseTenantBackend- Abstract backendDatabaseTenantBackend- Database isolationSchemaTenantBackend- Schema isolationCacheTenantBackend- Cache management
Utilities
get_tenant_model()- Get configured tenant modelget_domain_model()- Get configured domain modelget_tenant_backend()- Get backend for tenantget_current_tenant()- Get current tenant
Exceptions
TenantNotFound- Tenant resolution failedDomainNotFound- Domain resolution failed
See full API documentation for complete reference.
Common Patterns
Multi-Tenant Serializers (DRF)
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 data
Cross-Tenant Queries
from 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")
Tenant Migration Hooks
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
Performance Considerations
- 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
Security Best Practices
- 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
Troubleshooting
Tenant Not Found
# Error: TenantNotFound
# Solution: Verify TENANT_RESOLVER configuration and domain mapping
Query Runs on Wrong Database
# Error: Data queried from wrong tenant
# Solution: Ensure TenantMiddleware is in MIDDLEWARE
# or explicitly use TenantContext.use_tenant()
Migrations Fail
# Error: Migration fails for specific tenant
# Solution: Run with --no-input flag
python manage.py migratetenant --tenant-id=acme --no-input
See Troubleshooting Guide for more.
Development
Running Tests
# 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
Code Style
- Follow PEP 8
- Use Black for formatting
- Use isort for import sorting
- Type hints required for public APIs
Contributing
- 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.
Changelog
See CHANGELOG.md for release notes and breaking changes.
Roadmap
- Hybrid isolation strategy (database + schema)
- REST API for tenant management
- Tenant analytics dashboard
- Performance monitoring tools
- Multi-database support (MySQL, Oracle)
License
This project is licensed under the MIT License - see LICENSE file for details.
Support
Getting Help
- Documentation: Read the Docs
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Reporting Issues
Please include:
- Django version
- Python version
- Isolation strategy used
- Minimal reproducible example
- Error traceback
Citation
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},
}
Acknowledgments
- 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_omnitenant-1.0.1.tar.gz.
File metadata
- Download URL: django_omnitenant-1.0.1.tar.gz
- Upload date:
- Size: 132.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1385e4a44599cf3890287316b9d6e5476e0b7db7ea94d1e54e2ea2adef22854d
|
|
| MD5 |
cb421ccaa56976e1204b3a542211f27f
|
|
| BLAKE2b-256 |
694f26b509f681a85d6b87e7ed7300e675ac241052ca65aec4bee79f8d16b7dc
|
File details
Details for the file django_omnitenant-1.0.1-py3-none-any.whl.
File metadata
- Download URL: django_omnitenant-1.0.1-py3-none-any.whl
- Upload date:
- Size: 151.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acf2f3ee48e7d08447cbc8799741927084e206b535fc7bf1c37e697bd17a1bbc
|
|
| MD5 |
9f2ff7a345ed0814049b5497bffe9675
|
|
| BLAKE2b-256 |
0e097d7e7e0a53897763c04af17f37076717478984b97b544ca939cf50f4d2ed
|