Skip to main content

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) or database (separate DB per tenant), switched by a single setting
  • Tenant resolution — X-Tenant header (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 — TenantDatabaseRouter for 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

Metadata

Release files for django-tenantkit 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-tenantkit 0.1.3
File Size Uploaded
django_tenantkit-0.1.3.tar.gz 27.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-tenantkit 0.1.3
File Interpreter ABI Platform
django_tenantkit-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 53.7 kB

Release files / django_tenantkit-0.1.3.tar.gz

Download URL django_tenantkit-0.1.3.tar.gz
Size 27.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f89dbb7ee1a78e4c8ebd4ab59486dc9e5ce4c522af567cd5ba16ad02962842bf
BLAKE2b-256 checksum
How to use checksums
fa4c8a8af996dc9bbcd2a951e624771f7430522f57d654f6949fe29962aab77b
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 Mar 12, 2026.

Transparency log

Release files / django_tenantkit-0.1.3-py3-none-any.whl

Download URL django_tenantkit-0.1.3-py3-none-any.whl
Size 25.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ba7a2527be10309243a0da07d845eb1fcd9ed317eef28fa2cc6f6bba20f74f93
BLAKE2b-256 checksum
How to use checksums
11150a9580812a1e1a7ea0845e6c184e907b1cb3bd0165fc6867554582e07629
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 Mar 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page