Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

django-omnitenant

Python Django License PyPI

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 interactively
  • createtenantsuperuser - Create admin users per tenant
  • migratetenant - Run migrations for specific tenants
  • migratealltenants - Batch migrate all tenants
  • shell - Django shell with tenant context
  • showtenants - 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',
    # ...
    'myapp',  # Your application
    'django_omnitenant',
]

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 domain
  • django_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

Backends

Utilities

Exceptions

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_AGE in 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() and bulk_update() in tenant context

Security Best Practices

  1. Validate Tenant Access: Always verify tenant context in views
  2. Secure Credentials: Store DB credentials securely (env variables, vaults)
  3. Audit Logging: Log cross-tenant operations
  4. Rate Limiting: Implement per-tenant rate limiting
  5. Data Isolation: Verify isolation with security tests
  6. Admin Access: Restrict admin to master tenant
  7. 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

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit changes (git commit -m 'Add amazing feature')
  4. Push to branch (git push origin feature/amazing-feature)
  5. 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

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages