Skip to main content

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 TenantMixin for 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

djust_tenants-0.3.0.tar.gz (27.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

djust_tenants-0.3.0-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

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

Hashes for djust_tenants-0.3.0.tar.gz
Algorithm Hash digest
SHA256 4a1c64650502c80860970ddc34ceafd058a2c41d5564a1f14ad261be13091c84
MD5 449f8998ba1c16b168bd27de7a80cece
BLAKE2b-256 67f74a54c3d4749ddc4b750066fbc1bf3e91420582116e6c8c2edcfcf5519781

See more details on using hashes here.

Provenance

The following attestation bundles were made for djust_tenants-0.3.0.tar.gz:

Publisher: publish.yml on djust-org/djust-tenants

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for djust_tenants-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c4434707e0b76f7a63db896ef6ef5134bf7993fb52b246100149ea491690c29c
MD5 be038aaeb8fe4b31db1b433618d088e9
BLAKE2b-256 5517594899525a5df67d3a098cd3a5f1ef785b25acd06c94b1a12fd6232faf43

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page