django-tenantsdjango-tenants Worksdjango-tenants enables building SaaS-style applications where:
This approach is called:
Shared Database + Separate Schemas
A tenant represents:
inside a SaaS application.
Example:
| Tenant | Subdomain |
|---|---|
| Ferrari | ferrari.myapp.com |
| McLaren | mclaren.myapp.com |
Each tenant should only access its own data.
Tenant A -> Django App A -> DB A
Tenant B -> Django App B -> DB B
One Django App
|
One PostgreSQL Database
|
+-------------------+
| public schema |
| ferrari schema |
| mclaren schema |
+-------------------+
All tenants use same tables
tenant_id column separates data
Benefits:
Official Docs:
https://django-tenants.readthedocs.io/
Represents a customer/client.
Example:
class Client(TenantMixin):
name = models.CharField(max_length=100)
Maps subdomain -> tenant.
Example:
ferrari.localhost -> Ferrari Tenant
PostgreSQL namespace containing tables.
Example:
public
ferrari
mclaren
Apps stored in public schema.
Examples:
Apps stored inside tenant schemas.
Examples:
High-level flow:
Schemas behave like folders.
Example:
Database
├── public
├── ferrari
└── mclaren
Each schema contains its own tables.
ferrari.projects
ferrari.tasks
mclaren.projects
mclaren.tasks
Same table names.
Different data.
pip install django-tenants psycopg2-binary
or
uv add django-tenants
django-tenants requires PostgreSQL.
DATABASES = {
"default": {
"ENGINE": "django_tenants.postgresql_backend",
}
}
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
MUST be first.
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
]
SHARED_APPS = (
'django_tenants',
'customers',
'django.contrib.contenttypes',
'django.contrib.auth',
)
These tables live in:
public schema
TENANT_APPS = (
'projects',
'tasks',
)
These tables live in:
tenant schemas
INSTALLED_APPS = list(SHARED_APPS) + [
app for app in TENANT_APPS
if app not in SHARED_APPS
]
from django_tenants.models import TenantMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
auto_create_schema = True
from django_tenants.models import DomainMixin
class Domain(DomainMixin):
pass
TENANT_MODEL = "customers.Client"
TENANT_DOMAIN_MODEL = "customers.Domain"
auto_create_schema = True
When tenant saved:
Core middleware:
TenantMainMiddleware
Responsibilities:
request.tenant
Example:
def dashboard(request):
print(request.tenant)
Never use:
python manage.py migrate
Use:
python manage.py migrate_schemas
python manage.py migrate_schemas --shared
No special ORM logic needed.
Project.objects.all()
Automatically queries:
current tenant schema
Project.objects.create(
name="Win Championship"
)
Automatically saves into current tenant schema.
Use:
from django_tenants.admin import TenantAdminMixin
Example:
@admin.register(Client)
class ClientAdmin(TenantAdminMixin, admin.ModelAdmin):
pass
Very important.
Without authentication:
You MUST implement:
User belongs to Tenant
class Membership(models.Model):
user = models.ForeignKey(User)
tenant = models.ForeignKey(Client)
Required for schemas.
Only global/shared data.
Examples:
Examples:
Best routing strategy.
Examples:
tenant1.myapp.com
tenant2.myapp.com
Critical for security.
Celery tasks need schema context.
Thousands of schemas can impact performance.
Always start with:
AbstractUser
Example:
*.myapp.com
Use:
Use wildcard certificates.
Example:
*.myapp.com
Backup strategy should include:
Validate:
Ensure:
user belongs to request.tenant
Never manually override schema without caution.
Celery tasks do not have request object.
Therefore:
request.tenant
is unavailable.
Pass schema explicitly.
Example:
from django_tenants.utils import tenant_context
with tenant_context(tenant):
# tenant-aware logic
This is the #1 mistake.
Not supported.
Incorrect separation can break architecture.
You must create:
public schema tenant
Tenant middleware must be FIRST.
Example:
SaaS CRM
users
subscriptions
billing
tenant management
customers
leads
projects
notes
tasks
project/
├── customers/
│ ├── models.py
│ ├── admin.py
│
├── projects/
├── tasks/
├── config/
│ ├── settings.py
│ ├── urls.py
Use:
tenant.localhost
Use PostgreSQL container.
Recommended for large SaaS platforms.
SHOW search_path;
SET search_path TO ferrari;
SET search_path TO ferrari, public;
The MOST important concept:
django-tenants changes PostgreSQL search_path
based on incoming request domain
Everything else builds on this.
Official Documentation:
https://django-tenants.readthedocs.io/
Main Concepts Covered From Transcript: