djust-tenants
Multi-tenancy package for Django and djust applications. Build SaaS apps with tenant isolation via subdomains, paths, headers, or custom resolution strategies.
Features
- 🏢 Flexible Tenant Resolution - Subdomain, path, header, session, or custom resolvers
- 🔒 Data Isolation - Schema-based (PostgreSQL) or FK-based (any database)
- ⚡ LiveView Integration - Optional
TenantMixinfor djust LiveViews - 🗄️ Tenant-Scoped Backends - Redis, memory, or database backends with tenant namespacing
- 🎯 Zero Config - Works with existing Django models or bring your own tenant model
- 🧪 Well Tested - Comprehensive test suite with real-world examples
Installation
# Core (works with any Django project)
pip install djust-tenants
# With djust LiveView integration
pip install djust-tenants[djust]
# With Redis backend
pip install djust-tenants[redis]
# With PostgreSQL schema isolation
pip install djust-tenants[postgres]
# Everything
pip install djust-tenants[djust,redis,postgres]
Quick Start
1. Add to Django Settings
# settings.py
INSTALLED_APPS = [
# ...
"djust_tenants",
]
MIDDLEWARE = [
# ...
"djust_tenants.middleware.TenantMiddleware", # Add after SessionMiddleware
]
# Tenant resolution strategy
DJUST_TENANTS = {
"RESOLVER": "subdomain", # subdomain, path, header, session, or custom
"MAIN_DOMAIN": "myapp.com",
"SUBDOMAIN_EXCLUDE": ["www", "api", "admin"],
}
2. Create Tenant Model (Optional)
# myapp/models.py
from django.db import models
class Organization(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True) # Used for subdomain
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
3. Use in Views
Standard Django View
from django.views import View
class DashboardView(View):
def get(self, request):
# request.tenant is automatically set by middleware
tenant = request.tenant
# Query data scoped to current tenant
projects = Project.objects.filter(tenant_id=tenant.id)
return render(request, 'dashboard.html', {
'tenant': tenant,
'projects': projects,
})
djust LiveView
from djust import LiveView
from djust_tenants.mixins import TenantMixin
class DashboardView(TenantMixin, LiveView):
template_name = 'dashboard.html'
def mount(self, request, **kwargs):
# self.tenant is automatically set by TenantMixin
self.projects = Project.objects.filter(tenant=self.tenant.obj)
def get_context_data(self, **kwargs):
return {
'tenant': self.tenant,
'projects': self.projects,
}
Tenant Resolution Strategies
Subdomain Routing
acme.myapp.com → Organization(slug='acme')
startup.myapp.com → Organization(slug='startup')
# settings.py
DJUST_TENANTS = {
"RESOLVER": "subdomain",
"MAIN_DOMAIN": "myapp.com",
"SUBDOMAIN_EXCLUDE": ["www", "api", "admin"],
}
Path-Based Routing
myapp.com/acme/dashboard → Organization(slug='acme')
myapp.com/startup/reports → Organization(slug='startup')
# settings.py
DJUST_TENANTS = {
"RESOLVER": "path",
"PATH_POSITION": 1, # /org_slug/...
"PATH_EXCLUDE": ["admin", "api", "static"],
}
Header-Based Routing
X-Tenant-ID: acme → Organization(slug='acme')
# settings.py
DJUST_TENANTS = {
"RESOLVER": "header",
"HEADER_NAME": "X-Tenant-ID",
}
Session-Based Routing
# settings.py
DJUST_TENANTS = {
"RESOLVER": "session",
"SESSION_KEY": "tenant_id",
}
# In view:
request.session['tenant_id'] = organization.id
Custom Resolver
# myapp/tenants.py
from djust_tenants.resolvers import TenantResolver, TenantInfo
class CustomResolver(TenantResolver):
def resolve(self, request):
# Your custom logic here
tenant_id = request.GET.get('tenant')
if tenant_id:
org = Organization.objects.get(id=tenant_id)
return TenantInfo(
id=str(org.id),
name=org.name,
slug=org.slug,
obj=org,
)
return None
# settings.py
DJUST_TENANTS = {
"RESOLVER": "custom",
"CUSTOM_RESOLVER": "myapp.tenants.CustomResolver",
}
Data Isolation Strategies
FK-Based (Works with Any Database)
from django.db import models
class Project(models.Model):
tenant = models.ForeignKey('myapp.Organization', on_delete=models.CASCADE)
name = models.CharField(max_length=200)
# All queries must filter by tenant
projects = Project.objects.filter(tenant=request.tenant.obj)
Pros: Simple, works everywhere, easy migrations Cons: Shared tables, risk of cross-tenant leaks
Schema-Based (PostgreSQL Only)
# settings.py
DATABASES = {
'default': {
'ENGINE': 'djust_tenants.backends.postgres',
'NAME': 'myapp',
'TENANT_SCHEMA_PREFIX': 'tenant_',
}
}
# Each tenant gets a separate schema:
# public schema (shared: users, organizations)
# tenant_acme schema (acme's data)
# tenant_startup schema (startup's data)
# Queries automatically scoped to current schema
projects = Project.objects.all() # SELECT * FROM tenant_acme.projects
Pros: Strong isolation, great for compliance/security Cons: PostgreSQL only, complex migrations
Tenant-Scoped Backends
Redis Backend (Namespaced Keys)
# settings.py
DJUST_TENANTS = {
"REDIS_URL": "redis://localhost:6379/0",
}
# In your code
from djust_tenants.backends import get_tenant_redis
def my_view(request):
redis = get_tenant_redis(request.tenant)
# Keys are automatically namespaced: tenant:{tenant_id}:mykey
redis.set('mykey', 'myvalue')
value = redis.get('mykey')
Memory Backend (Isolated Storage)
from djust_tenants.backends import get_tenant_memory
def my_view(request):
storage = get_tenant_memory(request.tenant)
storage['mykey'] = 'myvalue'
value = storage.get('mykey')
Template Usage
{# dashboard.html #}
<h1>{{ tenant.name }} Dashboard</h1>
<p>Tenant ID: {{ tenant.id }}</p>
<p>Slug: {{ tenant.slug }}</p>
{% for project in projects %}
<div>{{ project.name }}</div>
{% endfor %}
Testing
from django.test import TestCase
from djust_tenants.test import TenantTestCase
class MyTestCase(TenantTestCase):
def setUp(self):
super().setUp()
# self.tenant is automatically created
def test_tenant_isolation(self):
# Create data in current tenant
project = Project.objects.create(
tenant=self.tenant.obj,
name='Test Project'
)
# Switch to different tenant
other_tenant = self.create_tenant(slug='other')
self.set_tenant(other_tenant)
# Should not see previous tenant's data
self.assertEqual(Project.objects.count(), 0)
Examples
See the examples/ directory for complete working examples:
- examples/simple_saas/ - Minimal SaaS app with subdomain routing
- examples/path_based/ - Path-based multi-tenancy
- examples/djust_integration/ - Full djust LiveView integration
- examples/schema_isolation/ - PostgreSQL schema-based isolation
Advanced Usage
Tenant Manager (Auto-Filter Querysets)
from djust_tenants.managers import TenantManager
class Project(models.Model):
tenant = models.ForeignKey('Organization', on_delete=models.CASCADE)
name = models.CharField(max_length=200)
objects = TenantManager() # Auto-filters by current tenant
# In view with request.tenant set:
projects = Project.objects.all() # Automatically filtered by tenant
Management Commands
# Run command for specific tenant
python manage.py my_command --tenant=acme
# Run command for all tenants
python manage.py my_command --all-tenants
Audit Logging
from djust_tenants.middleware import get_current_tenant
def my_view(request):
tenant = get_current_tenant()
# Log with tenant context
logger.info("User action", extra={
'tenant_id': tenant.id,
'tenant_name': tenant.name,
})
Configuration Reference
DJUST_TENANTS = {
# Resolver type
"RESOLVER": "subdomain", # subdomain, path, header, session, custom
# Subdomain options
"MAIN_DOMAIN": "myapp.com",
"SUBDOMAIN_EXCLUDE": ["www", "api", "admin"],
# Path options
"PATH_POSITION": 1, # URL segment position (0-indexed)
"PATH_EXCLUDE": ["admin", "api", "static"],
# Header options
"HEADER_NAME": "X-Tenant-ID",
# Session options
"SESSION_KEY": "tenant_id",
# Custom resolver
"CUSTOM_RESOLVER": "myapp.tenants.CustomResolver",
# Behavior
"REQUIRED": True, # Raise 404 if no tenant found
"DEFAULT": None, # Default tenant if none resolved
"CONTEXT_NAME": "tenant", # Template context variable name
# Backends
"REDIS_URL": "redis://localhost:6379/0",
}
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
MIT License - see LICENSE for details.
Links
- Documentation: https://docs.djust.org/tenants
- GitHub: https://github.com/djust-org/djust-tenants
- Issues: https://github.com/djust-org/djust-tenants/issues
- djust Framework: https://github.com/djust-org/djust
Metadata
Release files for djust-tenants 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| djust_tenants-0.3.0.tar.gz | 27.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| djust_tenants-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 48.9 kB
Release files / djust_tenants-0.3.0.tar.gz
| Download URL | djust_tenants-0.3.0.tar.gz |
|---|---|
| Size | 27.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4a1c64650502c80860970ddc34ceafd058a2c41d5564a1f14ad261be13091c84
|
|
BLAKE2b-256 checksum How to use checksums |
67f74a54c3d4749ddc4b750066fbc1bf3e91420582116e6c8c2edcfcf5519781
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 24, 2026.
Transparency logRelease files / djust_tenants-0.3.0-py3-none-any.whl
| Download URL | djust_tenants-0.3.0-py3-none-any.whl |
|---|---|
| Size | 21.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c4434707e0b76f7a63db896ef6ef5134bf7993fb52b246100149ea491690c29c
|
|
BLAKE2b-256 checksum How to use checksums |
5517594899525a5df67d3a098cd3a5f1ef785b25acd06c94b1a12fd6232faf43
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 24, 2026.
Transparency log