Pluggable multi-tenant toolkit for Django — schema & database backends, JWT auth, tenant-scoped middleware.
Project description
django-tenantkit
Pluggable multi-tenant toolkit for Django. Wraps django-tenants and adds tenant-scoped JWT authentication, flexible tenant resolution, and a full middleware stack. Supports both schema-per-tenant and database-per-tenant modes.
Features
- Dual-mode tenancy —
schema(PostgreSQL schemas via django-tenants) ordatabase(separate DB per tenant), switched by a single setting - Tenant resolution —
X-Tenantheader (domain or schema name) with host domain fallback - JWT authentication — local token decoding, tenant-scoped user lookup, result caching
- Middleware stack —
TenantMiddleware→AuthMiddleware→BlockedUserMiddleware - DRF integration —
TenantAuthentication,TenantUser, permission classes - ASGI compatible — context storage uses
contextvars, safe under async Django - Database router —
TenantDatabaseRouterfor query routing across schemas/databases - Migration helpers — run public or tenant migrations in either mode
Installation
pip install django-tenantkit
Quick Start
1. Create Tenant and Domain models
# tenancy/models.py
from django.db import models
from tenantkit.tenancy.models import TenantMixin, DomainMixin
class Tenant(TenantMixin):
name = models.CharField(max_length=100)
auto_create_schema = True
class Domain(DomainMixin):
pass
2. Configure settings
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': 'your_db',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
'PORT': '5432',
}
}
INSTALLED_APPS = [
'django_tenants',
'tenancy',
'django.contrib.contenttypes',
'django.contrib.auth',
'rest_framework',
'tenantkit',
# ... your apps
]
TENANT_DB_MODE = 'schema' # or 'database'
TENANT_MODEL = 'tenancy.Tenant'
TENANT_DOMAIN_MODEL = 'tenancy.Domain'
TENANT_APPS = ['django.contrib.auth', ...] # apps in tenant schemas
SHARED_APPS = ['django_tenants', 'tenancy', ...] # apps in public schema
DATABASE_ROUTERS = [
'django_tenants.routers.TenantSyncRouter',
'tenantkit.tenancy.TenantDatabaseRouter',
]
3. Add middleware
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'tenantkit.tenancy.middleware.TenantMiddleware',
'tenantkit.auth.middleware.AuthMiddleware',
'tenantkit.auth.middleware.BlockedUserMiddleware',
# ... rest of middleware
]
4. DRF authentication (optional)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'tenantkit.auth.TenantAuthentication',
],
}
5. Run migrations and create a tenant
python manage.py migrate_schemas --shared
from tenancy.models import Tenant, Domain
public = Tenant(schema_name='public', name='Public')
public.save()
Domain.objects.create(domain='localhost', tenant=public, is_primary=True)
tenant = Tenant(schema_name='acme', name='Acme Corp')
tenant.save() # auto-creates schema and runs migrations
Domain.objects.create(domain='acme.localhost', tenant=tenant, is_primary=True)
Usage
Request attributes set by AuthMiddleware
request.user_info # {'id': '1', 'email': '...', 'roles': [...], ...}
request.user_id # '1'
request.user_email # 'test@acme.com'
request.user_roles # ['admin']
# DRF (via TenantAuthentication)
request.user.has_perm('orders.view_order')
request.user.is_superuser # True if 'superuser' in roles
Permissions
from tenantkit.auth.permissions import IsAuthenticated, IsSuperAdmin
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def protected_view(request): ...
@api_view(['GET'])
@permission_classes([IsSuperAdmin])
def admin_view(request): ...
Path exemptions
TENANT_EXEMPT_PATHS = ['/health/', '/admin/', '/docs/']
AUTH_EXEMPT_PATHS = ['/health/', '/admin/', '/docs/']
AUTH_OPTIONAL_PATHS = ['/api/catalog/'] # tenant required, auth optional
PUBLIC_GET_ENDPOINTS = ['/api/menu/'] # GET only, no auth, tenant required
Generating tokens (for testing)
import jwt
payload = {'user_id': str(user.id), 'exp': ...}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
Making requests
curl -H "X-Tenant: acme" \
-H "Authorization: Bearer eyJ..." \
http://localhost:8000/api/me/
Settings Reference
Tenancy
| Setting | Default | Description |
|---|---|---|
TENANT_DB_MODE |
'schema' |
'schema' or 'database' |
TENANT_MODEL |
'tenancy.Tenant' |
Your Tenant model |
TENANT_DOMAIN_MODEL |
'tenancy.Domain' |
Your Domain model |
TENANT_HEADER_NAME |
'X-Tenant' |
HTTP header for tenant resolution |
REQUIRE_TENANT_BY_DEFAULT |
True |
Reject requests without tenant context |
TENANT_EXEMPT_PATHS |
['/health/', ...] |
Paths that bypass tenant resolution |
TENANT_APPS |
[] |
App labels routed to tenant schemas/databases |
SHARED_APPS |
[] |
App labels routed to public schema / default database |
TENANT_DB_NAME_PREFIX |
'tenant_' |
Prefix for database names (database mode) |
TENANT_DB_TEMPLATE |
{} |
Extra DB config merged with default for tenant databases |
Authentication
| Setting | Default | Description |
|---|---|---|
JWT_SECRET_KEY |
SECRET_KEY |
Key used to verify JWT signatures |
JWT_ALGORITHM |
'HS256' |
JWT signing algorithm |
AUTH_CACHE_TIMEOUT |
300 |
Seconds to cache user data (capped to token lifetime) |
TENANT_USER_APP_LABEL |
'auth' |
App label of your User model |
TENANT_USER_MODEL_NAME |
'User' |
Model name of your User model |
REQUIRE_AUTH_BY_DEFAULT |
True |
Reject unauthenticated requests |
AUTH_EXEMPT_PATHS |
['/health/', ...] |
Paths that bypass authentication |
AUTH_OPTIONAL_PATHS |
[] |
Paths where auth is optional |
Error Responses
All errors return JSON:
{"success": false, "code": "ERROR_CODE", "error": {"message": "...", "hint": "..."}}
| Code | Status | When |
|---|---|---|
MISSING_TENANT_HEADER |
400 | No X-Tenant and REQUIRE_TENANT_BY_DEFAULT=True |
INVALID_TENANT |
404 | X-Tenant value not found |
AUTHENTICATION_REQUIRED |
401 | No Bearer token |
TOKEN_EXPIRED |
401 | JWT exp in the past |
INVALID_SIGNATURE |
401 | Wrong signing secret |
USER_NOT_FOUND |
401 | user_id not in tenant DB |
ACCOUNT_BLOCKED |
401 | is_blocked=True |
Running Tests
pip install pytest
pytest tests/ -v
Requirements
- Python >= 3.9
- Django >= 4.2
- django-tenants >= 3.8
- Django REST Framework >= 3.14
- PyJWT >= 2.0
- PostgreSQL
License
MIT
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 django_tenantkit-0.1.3.tar.gz.
File metadata
- Download URL: django_tenantkit-0.1.3.tar.gz
- Upload date:
- Size: 27.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f89dbb7ee1a78e4c8ebd4ab59486dc9e5ce4c522af567cd5ba16ad02962842bf
|
|
| MD5 |
679b84becc4739dca55e7acba6d421d2
|
|
| BLAKE2b-256 |
fa4c8a8af996dc9bbcd2a951e624771f7430522f57d654f6949fe29962aab77b
|
Provenance
The following attestation bundles were made for django_tenantkit-0.1.3.tar.gz:
Publisher:
publish.yml on hannan665/django-tenantkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_tenantkit-0.1.3.tar.gz -
Subject digest:
f89dbb7ee1a78e4c8ebd4ab59486dc9e5ce4c522af567cd5ba16ad02962842bf - Sigstore transparency entry: 1091036446
- Sigstore integration time:
-
Permalink:
hannan665/django-tenantkit@0306a2ea7914b34e93f90694e2db39d1626c023e -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/hannan665
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0306a2ea7914b34e93f90694e2db39d1626c023e -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_tenantkit-0.1.3-py3-none-any.whl.
File metadata
- Download URL: django_tenantkit-0.1.3-py3-none-any.whl
- Upload date:
- Size: 25.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 |
ba7a2527be10309243a0da07d845eb1fcd9ed317eef28fa2cc6f6bba20f74f93
|
|
| MD5 |
1237e9dfcb8829de69bab5065131c3b2
|
|
| BLAKE2b-256 |
11150a9580812a1e1a7ea0845e6c184e907b1cb3bd0165fc6867554582e07629
|
Provenance
The following attestation bundles were made for django_tenantkit-0.1.3-py3-none-any.whl:
Publisher:
publish.yml on hannan665/django-tenantkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_tenantkit-0.1.3-py3-none-any.whl -
Subject digest:
ba7a2527be10309243a0da07d845eb1fcd9ed317eef28fa2cc6f6bba20f74f93 - Sigstore transparency entry: 1091036449
- Sigstore integration time:
-
Permalink:
hannan665/django-tenantkit@0306a2ea7914b34e93f90694e2db39d1626c023e -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/hannan665
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0306a2ea7914b34e93f90694e2db39d1626c023e -
Trigger Event:
push
-
Statement type: