Skip to main content

A lightweight, API-first Django & Django REST Framework (DRF) multi-tenancy schema isolation package.

Project description

django-rest-pgtenants

A lightweight, native multi-tenancy package for Django and Django REST Framework (DRF) using PostgreSQL schema isolation. Purpose-built for REST APIs and asynchronous background workers (like Celery) without requiring heavy database engine overrides, monkeypatching, or custom database backends.


🚀 Features

  • Native Django Approach: Zero monkeypatching. It runs entirely on Django's official database routing and middleware API.
  • REST API First: Pluggable middlewares for JWT/OIDC token claims and custom HTTP headers.
  • Thread & Async Safe: Uses Python's native contextvars.ContextVar instead of threading.local, preventing context leaking across concurrent async requests or workers.
  • Fail-Safe Security: Tenant-specific tables are never created in the shared public schema. Any query executed without a tenant context will instantly fail with relation does not exist instead of leaking global data.
  • Automatic Cache Flushing: Automatically flushes Django's internal ContentType and permission cache on every schema switch to prevent cross-tenant permission leakages.
  • Worker & Script Support: A clean, context-manager wrapper tenant_context for background tasks, scripts, and shell commands.
  • Granular Tenant Migrations: A robust migrate_tenants command supporting forward migrations, target migration rollbacks, and database zero-state resets.

🛠️ The Core Concept

In PostgreSQL, a single database can contain multiple isolated schemas (namespaces):

  • public schema: Stores shared/global tables (like your tenant catalog, global users, Django metadata).
  • Tenant schemas (e.g. tenant_acme, tenant_apple): Store tenant-specific data tables (e.g. projects, orders, members).

For every request, the middleware dynamically switches PostgreSQL's search_path:

SET search_path TO tenant_schema, public;

PostgreSQL will search for tables inside the tenant schema first, falling back to the public schema only if the table is not found.


📦 Installation

Install the package via pip (or add it to your requirements.txt):

pip install django-rest-pgtenants

⚙️ Configuration

1. Update settings.py

Register the package, database router, and define the apps isolated inside the schemas:

INSTALLED_APPS = [
    # Django core apps...
    'django.contrib.contenttypes',
    'django.contrib.auth',
    
    # Pluggable multi-tenancy
    'django_rest_pgtenants',
    
    # Your shared apps (e.g. global workspaces catalog)
    'api.workspaces',
    
    # Your tenant-specific apps (tables inside individual tenant schemas)
    'api.workspace_tenant',
]

# Register the database router
DATABASE_ROUTERS = ['django_rest_pgtenants.db_routers.TenantRouter']

# Configure django-rest-pgtenants settings
NATIVE_TENANT = {
    'TENANT_MODEL': 'workspaces.Workspace',          # Model storing tenant metadata (app_label.ModelName)
    'SCHEMA_COLUMN': 'schema_name',                  # Column storing the PostgreSQL schema name
    'TENANT_APPS': ['api.workspace_tenant'],         # Apps that should only exist inside tenant schemas
    
    # Pluggable Middleware Configuration (Optional)
    'TENANT_CLAIM': 'workspace_id',                  # JWT claim key to extract tenant value
    'TENANT_FIELD': 'id',                            # Model field on TENANT_MODEL to query the value against
    'TENANT_HEADER': 'X-Tenant-ID',                  # Request header to extract tenant value
}

2. Choose and Register Middleware

Select the routing strategy that fits your project and add it to MIDDLEWARE in settings.py:

Option A: JWT / Token-based Routing (API-First)

Decodes incoming JWT tokens, parses the tenant claim, and switches search paths.

MIDDLEWARE = [
    ...
    'django_rest_pgtenants.middleware.JWTWorkspaceSchemaMiddleware',
    ...
]

Option B: Subdomain-based Routing

Extracts subdomains (e.g., acme.example.com -> acme) and matches it to a tenant slug.

MIDDLEWARE = [
    ...
    'django_rest_pgtenants.middleware.SubdomainSchemaMiddleware',
    ...
]

Option C: Custom HTTP Header Routing

Resolves schemas by reading a specific HTTP request header (e.g., X-Tenant-ID).

MIDDLEWARE = [
    ...
    'django_rest_pgtenants.middleware.HeaderTenantSchemaMiddleware',
    ...
]

📖 Component Overview

1. Database Router (django_rest_pgtenants/db_routers.py)

Intercepts Django migrations. It blocks tenant-specific tables from being created in the public schema during regular python manage.py migrate commands. Tenant migrations must run through the custom runner command.

2. Schema Manager (django_rest_pgtenants/schema_manager.py)

Handles dynamic SQL schema creation, search_path switches, and exposes the tenant switching API.

3. Context Manager for Workers (Celery)

To write safe asynchronous tasks or background jobs, wrap your logic inside tenant_context:

from celery import shared_task
from django_rest_pgtenants.schema_manager import tenant_context
from api.workspace_tenant.models import WorkspaceMember

@shared_task
def process_workspace_reports(schema_name):
    # Switches search_path safely for this worker thread context block
    with tenant_context(schema_name):
        members = WorkspaceMember.objects.all()  # Queries the tenant's schema tables
        # ... process report

🗄️ Running Migrations

Public / Shared Schema Migrations

Run standard Django migrations for all shared apps (e.g. custom user profiles, billing/tenant records):

python manage.py migrate

Tenant Schema Migrations

To migrate all tenant schemas to the latest database migrations:

python manage.py migrate_tenants

Rolling Back a Migration

To rollback all tenants to a specific migration target (e.g., version 0002 or zero to rollback all tenant tables):

python manage.py migrate_tenants --target 0002
python manage.py migrate_tenants --target zero

🔒 Connection Pooling Safety

When using connection poolers like PgBouncer or Django's persistent connections (CONN_MAX_AGE), a database connection is reused for multiple HTTP requests.

django-rest-pgtenants automatically resets the search path:

  1. When a request starts, search path switches to: schema_name, public
  2. At the end of the request-response cycle (even if the view crashes), it resets the search path back to: public

This prevents a subsequent request from accidentally inheriting the database schema context of a previous tenant.


📄 License

This project is licensed under the MIT License.


🤝 Support & Contributions

While django-rest-pgtenants comes embedded with all the core features needed to build secure, robust Django and Django REST Framework (DRF) multi-tenant systems, your feedback and use-cases are highly welcome!

  • Got a Feature Request or found a Bug? Feel free to open an issue or start a discussion on our GitHub Repository.
  • Want to contribute? Pull requests are welcome! Feel free to fork the repository, make your changes, and submit a PR.
  • Direct Support / Inquiry: For custom support or feature additions, you can reach out directly at pankajjarial.dev@gmail.com.

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

django_rest_pgtenants-0.1.0.tar.gz (11.7 kB view details)

Uploaded Source

Built Distribution

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

django_rest_pgtenants-0.1.0-py3-none-any.whl (10.9 kB view details)

Uploaded Python 3

File details

Details for the file django_rest_pgtenants-0.1.0.tar.gz.

File metadata

  • Download URL: django_rest_pgtenants-0.1.0.tar.gz
  • Upload date:
  • Size: 11.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_rest_pgtenants-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6c3569aa93f23809f07217a4fdd9ceee35b72837c398b4cf4c68fb82b499adae
MD5 11df799f270582688c83eb13899fcefd
BLAKE2b-256 743bfa6fe79126a38d6ff663aa16d908f4941dfcf34d3e39d221baa9555d10e6

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_rest_pgtenants-0.1.0.tar.gz:

Publisher: publish.yml on pankiae/django-rest-pgtenants

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

File details

Details for the file django_rest_pgtenants-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_rest_pgtenants-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a3dfbc56211de766713350a3c79bc8e0e7f4db9c4a5bd285e66801ce5d93106f
MD5 40aa5cb4de901418f4763a88cffab978
BLAKE2b-256 af0c1eeb79124396977f3419c715478a60abf8374fc2a91cc3e2e14ec8efdd5a

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_rest_pgtenants-0.1.0-py3-none-any.whl:

Publisher: publish.yml on pankiae/django-rest-pgtenants

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