Skip to content

Usage Guide

Overview

This guide covers common usage patterns for django-omnitenant. Learn how to create tenants, access tenant data, manage migrations, and handle advanced scenarios.

Table of Contents

Creating Tenants

Interactive Creation

The easiest way to create tenants is using the management command:

python manage.py createtenant

This launches an interactive prompt:

Starting tenant creation...
Enter tenant ID (unique): acme
Enter tenant name: ACME Corporation
Select isolation type (database/schema/table/cache): database
Do you want to create the database now? (y/n): y
Enter database name for tenant: acme_db
Enter database user: acme_user
Enter database password: ****
Enter database host: localhost
Enter database port (default: 5432): 5432
Do you want to run migrations for this tenant now? (y/n): y

Programmatic Creation

Create tenants in Python code:

from django_omnitenant.utils import get_tenant_model, get_tenant_backend

Tenant = get_tenant_model()

# Create tenant record
tenant = Tenant.objects.create(
    tenant_id='acme',
    name='ACME Corporation',
    isolation_type=Tenant.IsolationType.DATABASE,
    config={
        'db_config': {
            'NAME': 'acme_db',
            'USER': 'acme_user',
            'PASSWORD': 'secure_password',
            'HOST': 'localhost',
            'PORT': '5432',
        }
    }
)

# Provision resources (create database/schema)
backend = get_tenant_backend(tenant)
backend.create(run_migrations=True)

Bulk Creation

Create multiple tenants:

from django_omnitenant.utils import get_tenant_model, get_tenant_backend

Tenant = get_tenant_model()

tenants_data = [
    {
        'tenant_id': 'acme',
        'name': 'ACME Corporation',
        'isolation_type': Tenant.IsolationType.DATABASE,
    },
    {
        'tenant_id': 'globex',
        'name': 'Globex Inc',
        'isolation_type': Tenant.IsolationType.SCHEMA,
    },
]

for data in tenants_data:
    tenant = Tenant.objects.create(**data)
    backend = get_tenant_backend(tenant)
    backend.create(run_migrations=True)
    print(f"Created tenant: {tenant.name}")

Tenant Models

BaseTenant Model

Define your tenant model by extending BaseTenant:

from django.db import models
from django_omnitenant.models import BaseTenant, BaseDomain

class Tenant(BaseTenant):
    """Organization/Customer tenant model."""

    description = models.TextField(blank=True)
    max_users = models.IntegerField(default=10)
    subscription_tier = models.CharField(
        max_length=20,
        choices=[('free', 'Free'), ('pro', 'Pro'), ('enterprise', 'Enterprise')],
        default='free'
    )
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

    def get_user_count(self):
        """Get number of users in this tenant."""
        from django.contrib.auth.models import User
        return User.objects.filter(profile__tenant=self).count()

BaseDomain Model

Map domains to tenants:

from django_omnitenant.models import BaseDomain

class Domain(BaseDomain):
    """Custom domain for tenant."""

    is_primary = models.BooleanField(default=False)
    ssl_certificate = models.TextField(blank=True)

    class Meta:
        unique_together = ('domain', 'tenant')

    def __str__(self):
        return f"{self.domain} -> {self.tenant.name}"

Register Models

Configure in settings.py:

OMNITENANT_CONFIG = {
    'TENANT_MODEL': 'myapp.Tenant',
    'DOMAIN_MODEL': 'myapp.Domain',
}

Accessing Tenant Data

In Views

Tenant is automatically available in request:

from django.shortcuts import render
from django.http import JsonResponse

def dashboard(request):
    # Tenant automatically resolved by middleware
    tenant = request.tenant

    return JsonResponse({
        'tenant_id': tenant.tenant_id,
        'tenant_name': tenant.name,
        'subscription': tenant.subscription_tier,
    })

In Models

Use TenantQuerySetManager for automatic scoping:

from django.db import models
from django_omnitenant.models import TenantQuerySetManager

class Project(models.Model):
    """Project belonging to a tenant."""
    tenant = models.ForeignKey('Tenant', on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    description = models.TextField()

    objects = TenantQuerySetManager()

    def __str__(self):
        return self.name

Query in tenant context:

from django_omnitenant.tenant_context import TenantContext

# All queries automatically filtered to current tenant
projects = Project.objects.all()
project = Project.objects.get(id=1)

Across Models

Query related data:

from django_omnitenant.tenant_context import TenantContext

# Using select_related for efficiency
projects = Project.objects.select_related('tenant').all()

# Using prefetch_related
projects = Project.objects.prefetch_related('tasks').all()

# Filtering
active_projects = Project.objects.filter(
    status='active'
).order_by('-created_at')

Tenant Context

Switching Tenants

Use context managers to switch tenant context:

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 context
with TenantContext.use_tenant(acme_tenant):
    # All queries use acme's database/schema
    projects = Project.objects.all()
    print(f"ACME has {projects.count()} projects")

# Context automatically restored
print("Back to original context")

Getting Current Tenant

from django_omnitenant.tenant_context import TenantContext

# In view
def my_view(request):
    current_tenant = request.tenant
    # or
    current_tenant = TenantContext.get_tenant()
    return JsonResponse({'tenant': current_tenant.tenant_id})

Master Database Access

Access shared/master database:

from django_omnitenant.tenant_context import TenantContext

# Access master database (shared between tenants)
with TenantContext.use_master_db():
    # Query master database
    all_tenants = Tenant.objects.all()
    for tenant in all_tenants:
        print(f"Tenant: {tenant.name}")

Schema Access

For schema-per-tenant isolation:

from django_omnitenant.tenant_context import TenantContext

# Access specific schema
with TenantContext.use_schema('tenant_acme'):
    # Queries run on tenant_acme schema
    projects = Project.objects.all()

Database Queries

Basic Queries

All queries automatically scoped to current tenant:

from myapp.models import Project, Task

# These automatically filter to current tenant
all_projects = Project.objects.all()
active_projects = Project.objects.filter(status='active')
project = Project.objects.get(id=1)
project_count = Project.objects.count()

Filtering

from datetime import timedelta
from django.utils import timezone

# Complex filtering
recent_projects = Project.objects.filter(
    created_at__gte=timezone.now() - timedelta(days=30),
    status='active'
).exclude(
    name__icontains='test'
)

# Q objects for complex conditions
from django.db.models import Q

projects = Project.objects.filter(
    Q(status='active') | Q(status='in_progress')
).order_by('-priority')

Aggregations

from django.db.models import Count, Sum, Avg

# Count projects
project_count = Project.objects.count()

# Sum values
total_hours = Task.objects.aggregate(
    total=Sum('hours_spent')
)['total'] or 0

# Average
avg_priority = Project.objects.aggregate(
    avg=Avg('priority')
)['avg']

# Group by
from django.db.models import Count
by_status = Project.objects.values('status').annotate(
    count=Count('id')
)
# Result: [{'status': 'active', 'count': 5}, ...]

Bulk Operations

from myapp.models import Project, Task

# Bulk create
projects_data = [
    Project(name='Project 1'),
    Project(name='Project 2'),
    Project(name='Project 3'),
]
Project.objects.bulk_create(projects_data)

# Bulk update
Project.objects.filter(
    status='inactive'
).update(status='archived')

# Delete
Project.objects.filter(
    created_at__lt=timezone.now() - timedelta(days=365)
).delete()

Managing Migrations

Create Migrations

# Create migrations for your app
python manage.py makemigrations myapp

# Show migration plan
python manage.py showmigrations myapp

Migrate Master Database

# Migrate shared/master database
python manage.py migrate

Migrate Single Tenant

# Migrate specific tenant
python manage.py migratetenant --tenant-id=acme

# Show migration plan
python manage.py migratetenant --tenant-id=acme --plan

# Migrate specific app only
python manage.py migratetenant --tenant-id=acme myapp

# Migrate to specific migration
python manage.py migratetenant --tenant-id=acme myapp 0002_auto

Migrate All Tenants

# Run migrations for all tenants
python manage.py migratealltenants

# Show progress
python manage.py migratealltenants --verbosity=2

# Skip confirmation
python manage.py migratealltenants --no-input

Migration Signals

Hook into migration process:

from django.dispatch import receiver
from django_omnitenant.signals import tenant_migrated

@receiver(tenant_migrated)
def post_migration_setup(sender, tenant, **kwargs):
    """Run after migrations complete."""
    # Create default data for tenant
    from myapp.models import Config

    Config.objects.get_or_create(
        tenant=tenant,
        defaults={'theme': 'default'}
    )

Admin Interface

Tenant-Aware Admin

Restrict admin access to tenant-specific data:

from django.contrib import admin
from django_omnitenant.admin import TenantRestrictAdminMixin
from myapp.models import Project

@admin.register(Project)
class ProjectAdmin(TenantRestrictAdminMixin, admin.ModelAdmin):
    list_display = ['name', 'status', 'created_at']
    list_filter = ['status', 'created_at']
    search_fields = ['name', 'description']

    # Admin access restricted to master tenant only
    # Tenant data filtered automatically in list view

Create Tenant Superuser

# Interactive
python manage.py createtenantsuperuser --tenant-id=acme

# Automated
python manage.py createtenantsuperuser \
  --tenant-id=acme \
  --username=admin \
  --email=admin@acme.com \
  --noinput

Custom Admin Mixin

from django.contrib import admin
from django_omnitenant.tenant_context import TenantContext

class TenantFilterMixin(admin.ModelAdmin):
    """Automatically filter admin queryset to current tenant."""

    def get_queryset(self, request):
        qs = super().get_queryset(request)

        # If not superuser, filter to current tenant
        if not request.user.is_superuser:
            tenant = TenantContext.get_tenant()
            if tenant:
                qs = qs.filter(tenant=tenant)

        return qs

Celery Integration

Basic Task

from celery import shared_task
from django_omnitenant.tenant_context import TenantContext
from django_omnitenant.utils import get_tenant_model

@shared_task
def send_notification(user_id, message):
    """Send notification to user."""
    from django.contrib.auth.models import User

    user = User.objects.get(id=user_id)
    # Task executes in tenant context (automatic)
    print(f"Sending to {user.email}: {message}")

Task with Tenant

from django_omnitenant.patches.celery import TenantAwareTask

@shared_task(base=TenantAwareTask)
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)

    # Task automatically runs in tenant context
    with TenantContext.use_tenant(tenant):
        from myapp.models import Project

        projects = Project.objects.all()
        for project in projects:
            # Process project
            pass

Queue Task

from myapp.tasks import send_notification, process_tenant_data

# Send task to queue
send_notification.delay(user_id=1, message='Hello!')

# Queue with tenant context
process_tenant_data.delay(tenant_id='acme')

Celery Beat (Scheduled Tasks)

from celery.schedules import crontab

CELERY_BEAT_SCHEDULE = {
    'process-all-tenants': {
        'task': 'myapp.tasks.process_all_tenants',
        'schedule': crontab(hour=2, minute=0),  # 2 AM daily
    },
}

Task implementation:

@shared_task
def process_all_tenants():
    """Process data for all tenants."""
    from django_omnitenant.utils import get_tenant_model

    Tenant = get_tenant_model()

    for tenant in Tenant.objects.all():
        process_tenant_data.delay(tenant_id=tenant.tenant_id)

Testing

Basic Test Case

from django.test import TestCase
from django_omnitenant.utils import get_tenant_model

class ProjectTestCase(TestCase):
    def setUp(self):
        """Set up test tenant."""
        Tenant = get_tenant_model()
        self.tenant = Tenant.objects.create(
            tenant_id='test',
            name='Test Tenant'
        )

    def test_project_creation(self):
        """Test creating project in tenant."""
        from myapp.models import Project
        from django_omnitenant.tenant_context import TenantContext

        with TenantContext.use_tenant(self.tenant):
            project = Project.objects.create(
                name='Test Project',
                status='active'
            )

            self.assertEqual(project.name, 'Test Project')
            self.assertTrue(Project.objects.filter(id=project.id).exists())

Multi-Tenant Tests

from django.test import TestCase
from django_omnitenant.utils import get_tenant_model
from django_omnitenant.tenant_context import TenantContext

class MultiTenantTestCase(TestCase):
    def setUp(self):
        """Create multiple tenants."""
        Tenant = get_tenant_model()

        self.acme = Tenant.objects.create(
            tenant_id='acme',
            name='ACME'
        )

        self.globex = Tenant.objects.create(
            tenant_id='globex',
            name='Globex'
        )

    def test_isolation(self):
        """Test data isolation between tenants."""
        from myapp.models import Project

        # Create project in ACME
        with TenantContext.use_tenant(self.acme):
            Project.objects.create(name='ACME Project')

        # Create project in Globex
        with TenantContext.use_tenant(self.globex):
            Project.objects.create(name='Globex Project')

        # Verify isolation
        with TenantContext.use_tenant(self.acme):
            self.assertEqual(Project.objects.count(), 1)
            self.assertEqual(Project.objects.first().name, 'ACME Project')

        with TenantContext.use_tenant(self.globex):
            self.assertEqual(Project.objects.count(), 1)
            self.assertEqual(Project.objects.first().name, 'Globex Project')

Test with Request

from django.test import Client, TestCase
from django_omnitenant.utils import get_tenant_model

class ViewTestCase(TestCase):
    def setUp(self):
        """Set up test tenant and client."""
        Tenant = get_tenant_model()
        Domain = get_domain_model()

        self.tenant = Tenant.objects.create(
            tenant_id='test',
            name='Test'
        )

        Domain.objects.create(
            domain='test.localhost',
            tenant=self.tenant
        )

        self.client = Client()

    def test_dashboard_view(self):
        """Test dashboard view with tenant context."""
        response = self.client.get(
            '/dashboard/',
            HTTP_HOST='test.localhost'
        )

        self.assertEqual(response.status_code, 200)
        self.assertIn('Test', response.content.decode())

Common Patterns

Create Tenant Signal

from django.dispatch import receiver
from django_omnitenant.signals import tenant_created
from myapp.models import TenantConfig

@receiver(tenant_created)
def setup_new_tenant(sender, tenant, **kwargs):
    """Initialize tenant configuration."""
    TenantConfig.objects.create(
        tenant=tenant,
        theme='default',
        language='en'
    )

    print(f"Tenant {tenant.name} initialized")

Custom Resolver

from django_omnitenant.resolvers.base import BaseTenantResolver
from django_omnitenant.exceptions import TenantNotFound
from django_omnitenant.utils import get_tenant_model

class HeaderTenantResolver(BaseTenantResolver):
    """Resolve tenant from HTTP header."""

    def resolve(self, request):
        tenant_id = request.headers.get('X-Tenant-ID')

        if not tenant_id:
            raise TenantNotFound("X-Tenant-ID header required")

        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.py:

OMNITENANT_CONFIG = {
    'TENANT_RESOLVER': 'myapp.resolvers.HeaderTenantResolver',
}

DRF Serializer

from rest_framework import serializers
from myapp.models import Project

class ProjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = Project
        fields = ['id', 'name', 'status', 'created_at']

    def validate_name(self, value):
        """Validate project name is unique in tenant."""
        if Project.objects.filter(name=value).exists():
            raise serializers.ValidationError("Name already exists in this tenant")
        return value

Cross-Tenant Reporting

from django_omnitenant.tenant_context import TenantContext
from django_omnitenant.utils import get_tenant_model
from myapp.models import Project

def get_all_tenant_stats():
    """Generate report across all tenants."""
    Tenant = get_tenant_model()
    stats = {}

    for tenant in Tenant.objects.all():
        with TenantContext.use_tenant(tenant):
            stats[tenant.tenant_id] = {
                'name': tenant.name,
                'projects': Project.objects.count(),
                'users': User.objects.count(),
            }

    return stats

Troubleshooting

Tenant Not Found

# Error: TenantNotFound exception
# Solution: Verify domain mapping
from django_omnitenant.utils import get_domain_model

Domain = get_domain_model()
# Check domain exists
print(Domain.objects.all())

Wrong Tenant Data

# Error: Queries return data from wrong tenant
# Solution: Ensure middleware is active
print(request.tenant)  # Should show current tenant

# Or use TenantContext explicitly
from django_omnitenant.tenant_context import TenantContext
tenant = TenantContext.get_tenant()
print(tenant)

Migrations Fail

# Error: Migration fails for specific tenant
# Solution: Run with verbose output
python manage.py migratetenant --tenant-id=acme --verbosity=2

# Or run step by step
python manage.py migratetenant --tenant-id=acme --plan

Next Steps