Skip to main content

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

Project details


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.

Supported by

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