A lightweight, native Django multi-tenant schema isolation framework.
Project description
django-rest-pgtenants
A lightweight, native Django multi-tenancy package for PostgreSQL using schema isolation. Purpose-built for REST APIs (DRF) and async background workers (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.ContextVarinstead ofthreading.local, preventing context leaking across concurrent async requests or workers. - Fail-Safe Security: Tenant-specific tables are never created in the shared
publicschema. Any query executed without a tenant context will instantly fail withrelation does not existinstead of leaking global data. - Automatic Cache Flushing: Automatically flushes Django's internal
ContentTypeand permission cache on every schema switch to prevent cross-tenant permission leakages. - Worker & Script Support: A clean, context-manager wrapper
tenant_contextfor background tasks, scripts, and shell commands. - Granular Tenant Migrations: A robust
migrate_tenantscommand 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):
publicschema: 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:
- When a request starts, search path switches to:
schema_name, public - 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.
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_rest_pgtenants-0.1.0rc2.tar.gz.
File metadata
- Download URL: django_rest_pgtenants-0.1.0rc2.tar.gz
- Upload date:
- Size: 10.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edd29660c59edfe9404b2e582a0fe09530e52783b930184744fcc018a690afce
|
|
| MD5 |
d552a09c187cb6ebd9a0621076e1dea8
|
|
| BLAKE2b-256 |
fc0ad9ea30facfdad1cf5d90734ab0fa567cc6ab526ca981a8092d095cd8ad41
|
Provenance
The following attestation bundles were made for django_rest_pgtenants-0.1.0rc2.tar.gz:
Publisher:
publish.yml on pankiae/django-rest-pgtenants
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_rest_pgtenants-0.1.0rc2.tar.gz -
Subject digest:
edd29660c59edfe9404b2e582a0fe09530e52783b930184744fcc018a690afce - Sigstore transparency entry: 2112507957
- Sigstore integration time:
-
Permalink:
pankiae/django-rest-pgtenants@b603c181e95fd3afa6a85a188c1ad2c2107e23a1 -
Branch / Tag:
refs/tags/v0.1.0rc2 - Owner: https://github.com/pankiae
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b603c181e95fd3afa6a85a188c1ad2c2107e23a1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_rest_pgtenants-0.1.0rc2-py3-none-any.whl.
File metadata
- Download URL: django_rest_pgtenants-0.1.0rc2-py3-none-any.whl
- Upload date:
- Size: 10.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e34677cf1aa19d20c710a2da3193912a32adf622f86a909c42181d6dff8f5fbd
|
|
| MD5 |
f89827d491c1b6847ac65f6c53dbb3c4
|
|
| BLAKE2b-256 |
54eb1b10e215563d1a314707492b387f362022ddd3e38d95ab0cb24088a1a103
|
Provenance
The following attestation bundles were made for django_rest_pgtenants-0.1.0rc2-py3-none-any.whl:
Publisher:
publish.yml on pankiae/django-rest-pgtenants
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_rest_pgtenants-0.1.0rc2-py3-none-any.whl -
Subject digest:
e34677cf1aa19d20c710a2da3193912a32adf622f86a909c42181d6dff8f5fbd - Sigstore transparency entry: 2112507982
- Sigstore integration time:
-
Permalink:
pankiae/django-rest-pgtenants@b603c181e95fd3afa6a85a188c1ad2c2107e23a1 -
Branch / Tag:
refs/tags/v0.1.0rc2 - Owner: https://github.com/pankiae
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b603c181e95fd3afa6a85a188c1ad2c2107e23a1 -
Trigger Event:
release
-
Statement type: