django-tenants
Django-tenants is a Python library that enables multi-tenancy for Django applications by leveraging PostgreSQL schemas. It allows a single Django project instance to serve multiple customers (tenants), each with isolated data, a crucial feature for Software-as-a-Service (SaaS) platforms. The library automates schema switching based on request hostnames, ensuring data isolation and efficient resource utilization. It is actively maintained, with the current stable version being 3.10.1, and receives regular updates to support new Django and Python versions. [1, 6, 7]
Warnings
- breaking `django-tenants` v3.x has aligned with newer Django and Python versions. Specifically, v3.10.0 and v3.8.0 added support for Django 5.x and Python 3.13, while dropping support for older Django versions (e.g., 3.x, 4.0) and Python versions (e.g., 3.8, 3.1) in previous v3.x releases. Always check release notes for specific version compatibility when upgrading. [15]
- breaking As of v3.0.0, `django-tenants` removed `psycopg2` as a direct dependency. While this provides flexibility, it means users must explicitly install a PostgreSQL adapter like `psycopg2-binary` or `psycopg3` for database connectivity. [13]
- gotcha The `auto_drop_schema` field on your `TenantMixin` model defaults to `False`. If you explicitly set it to `True`, deleting a tenant model instance through the ORM will *automatically drop its associated PostgreSQL schema* without further confirmation. This can lead to irreversible data loss. [3]
- gotcha Running the standard `python manage.py migrate` command will apply migrations to *both* shared and tenant schemas, which is often not the desired behavior. You must use `python manage.py migrate_schemas --shared` for shared apps and `python manage.py tenant_command migrate --schema=yourtenant` (or `all_tenants_command`) for tenant-specific apps. [5]
- gotcha For `request.tenant` to be available in your templates, `django.template.context_processors.request` must be included in the `context_processors` option within your `TEMPLATES` setting in `settings.py`. [5]
- gotcha When working with ASGI applications (e.g., Dpahne, Uvicorn) and custom tenant resolution logic outside of `TenantMainMiddleware`, relying on `thread_locals` or similar global state for tenant context can be problematic due to the asynchronous nature of ASGI servers. This can lead to incorrect tenant context being applied. [17]
Install
-
pip install django-tenants
Imports
- TenantMixin
from django_tenants.models import TenantMixin
- DomainMixin
from django_tenants.models import DomainMixin
- TenantMainMiddleware
from django_tenants.middleware.main import TenantMainMiddleware
- TenantSyncRouter
from django_tenants.routers import TenantSyncRouter
- schema_context
from django_tenants.utils import schema_context
- tenant_context
from django_tenants.utils import tenant_context
- TenantAdminMixin
from django_tenants.admin import TenantAdminMixin
Quickstart
import os
import django
from django.conf import settings
from django.core.management import call_command
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project_name.settings')
django.setup()
# Assuming you have an app 'customers' with Client (TenantMixin) and Domain (DomainMixin) models
# from customers.models import Client, Domain # Uncomment in your actual project
# --- Example of creating a public tenant (adapt to your models) ---
# In your settings.py:
# TENANT_MODEL = "customers.Client"
# TENANT_DOMAIN_MODEL = "customers.Domain"
# Create a dummy Client and Domain for demonstration if not already existing
# (In a real scenario, this would involve your actual Client and Domain models)
class MockClient(object):
id = 1 # Dummy ID
schema_name = 'public'
name = 'Public Tenant'
class MockDomain(object):
domain = 'localhost'
tenant = MockClient()
is_primary = True
print("1. Ensure settings are configured (SHARED_APPS, TENANT_APPS, MIDDLEWARE, DATABASE_ROUTERS).")
print("2. Run initial migrations for shared schema:")
try:
# In a real project, this would be `call_command('migrate_schemas', '--shared')`
# For this quickstart, we'll simulate output, as actual migration requires a full Django setup
print(" Simulating: python manage.py migrate_schemas --shared")
# call_command('migrate_schemas', '--shared', verbosity=0)
print(" Shared schema migrations complete.")
except Exception as e:
print(f" Error during shared migrations (expected if not a full Django setup): {e}")
print("3. Create a tenant (e.g., in a Django shell or a management command):")
try:
# Example of creating a tenant (replace with your actual Client/Domain models and logic)
# tenant = Client(schema_name='tenant1', name='Tenant One', paid_until='2030-12-31', on_trial=False)
# tenant.save() # This automatically creates and syncs the schema
# domain = Domain()
# domain.domain = 'tenant1.localhost'
# domain.tenant = tenant
# domain.is_primary = True
# domain.save()
print(" Simulating tenant creation:")
print(" client = Client(schema_name='tenant1', name='Tenant One', ...)")
print(" client.save() # Schema 'tenant1' created and migrated automatically")
print(" domain = Domain(domain='tenant1.localhost', tenant=client, is_primary=True)")
print(" domain.save()")
print(" Tenant 'tenant1' created with domain 'tenant1.localhost'.")
except Exception as e:
print(f" Error during tenant creation (expected if not a full Django setup): {e}")
print("4. Access tenant-specific data via hostname (e.g., tenant1.localhost:8000).")
print(" The TenantMainMiddleware will automatically switch the database schema.")