Skip to main content

A Django app to allow multi-tenancy.

Project description

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',
    # ...
    '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 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_omnitenant-1.0.1.tar.gz (132.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_omnitenant-1.0.1-py3-none-any.whl (151.2 kB view details)

Uploaded Python 3

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

Hashes for django_omnitenant-1.0.1.tar.gz
Algorithm Hash digest
SHA256 1385e4a44599cf3890287316b9d6e5476e0b7db7ea94d1e54e2ea2adef22854d
MD5 cb421ccaa56976e1204b3a542211f27f
BLAKE2b-256 694f26b509f681a85d6b87e7ed7300e675ac241052ca65aec4bee79f8d16b7dc

See more details on using hashes here.

File details

Details for the file django_omnitenant-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_omnitenant-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 acf2f3ee48e7d08447cbc8799741927084e206b535fc7bf1c37e697bd17a1bbc
MD5 9f2ff7a345ed0814049b5497bffe9675
BLAKE2b-256 0e097d7e7e0a53897763c04af17f37076717478984b97b544ca939cf50f4d2ed

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page