Multi-tenancy package for Django and djust applications
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file djust_tenants-0.3.0.tar.gz.
File metadata
- Download URL: djust_tenants-0.3.0.tar.gz
- Upload date:
- Size: 27.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a1c64650502c80860970ddc34ceafd058a2c41d5564a1f14ad261be13091c84
|
|
| MD5 |
449f8998ba1c16b168bd27de7a80cece
|
|
| BLAKE2b-256 |
67f74a54c3d4749ddc4b750066fbc1bf3e91420582116e6c8c2edcfcf5519781
|
Provenance
The following attestation bundles were made for djust_tenants-0.3.0.tar.gz:
Publisher:
publish.yml on djust-org/djust-tenants
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
djust_tenants-0.3.0.tar.gz -
Subject digest:
4a1c64650502c80860970ddc34ceafd058a2c41d5564a1f14ad261be13091c84 - Sigstore transparency entry: 985840418
- Sigstore integration time:
-
Permalink:
djust-org/djust-tenants@e9ac8945a1dd64f1033ba75919ccdaa0d373fb00 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/djust-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e9ac8945a1dd64f1033ba75919ccdaa0d373fb00 -
Trigger Event:
release
-
Statement type:
File details
Details for the file djust_tenants-0.3.0-py3-none-any.whl.
File metadata
- Download URL: djust_tenants-0.3.0-py3-none-any.whl
- Upload date:
- Size: 21.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4434707e0b76f7a63db896ef6ef5134bf7993fb52b246100149ea491690c29c
|
|
| MD5 |
be038aaeb8fe4b31db1b433618d088e9
|
|
| BLAKE2b-256 |
5517594899525a5df67d3a098cd3a5f1ef785b25acd06c94b1a12fd6232faf43
|
Provenance
The following attestation bundles were made for djust_tenants-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on djust-org/djust-tenants
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
djust_tenants-0.3.0-py3-none-any.whl -
Subject digest:
c4434707e0b76f7a63db896ef6ef5134bf7993fb52b246100149ea491690c29c - Sigstore transparency entry: 985840446
- Sigstore integration time:
-
Permalink:
djust-org/djust-tenants@e9ac8945a1dd64f1033ba75919ccdaa0d373fb00 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/djust-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e9ac8945a1dd64f1033ba75919ccdaa0d373fb00 -
Trigger Event:
release
-
Statement type: