django-tenants

PostgreSQL schema-based multi-tenancy for Django, with per-tenant isolation on one shared database.

Library
PyPI
v3.14.0
1,884stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
92/100Excellent
Development Activity96
Maintenance84
Community88
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
79/100Good
Architecture88
Code Quality85
Innovation82
Learning Curve60

django-tenants gives Django applications multi-tenancy by mapping each tenant to its own PostgreSQL schema inside a single shared database. Incoming requests are matched to a tenant by hostname, and a custom database backend flips the connection’s search_path so every subsequent query for that request runs against the tenant’s schema automatically, with no changes required to views, querysets, or model code.

The package ships a custom django_tenants.postgresql_backend database engine, a TenantSyncRouter that decides which apps sync to the shared public schema versus per-tenant schemas, tenant-aware middleware for hostname and subfolder-based routing, and TenantMixin/DomainMixin model base classes for defining the tenant and domain models. A migrate_schemas management command runs migrations across every tenant schema, with pluggable standard, multiprocessing, and subprocess executors for scaling that step across large tenant counts. A bundled schema-cloning routine (adapted from the pg-clone-schema project) lets new tenants be created by copying a template schema’s structure and data instead of running migrations from scratch.

What You Get

  • A custom django_tenants.postgresql_backend database engine that swaps the connection’s search_path per request so ORM calls transparently hit the current tenant’s schema.
  • TenantMixin and DomainMixin abstract models for defining your tenant and domain models, with automatic schema creation/deletion wired into save()/delete().
  • Hostname- and subfolder-based tenant-routing middleware (TenantMainMiddleware, subfolder middleware) that resolves the current tenant from the incoming request.
  • A migrate_schemas management command with standard, multiprocessing, and subprocess executors for running migrations across many tenant schemas efficiently.
  • Fast tenant provisioning via schema cloning (copying a template schema’s structure and optionally its data) instead of replaying the full migration history for every new tenant.
  • A TenantSyncRouter and SHARED_APPS/TENANT_APPS settings split so shared reference apps and tenant-specific apps sync to the correct schemas.

Common Use Cases

  • Building a B2B SaaS product where each customer organization needs isolated data without provisioning a separate database per customer.
  • Migrating a single-tenant Django app to multi-tenant while keeping one shared codebase, one set of migrations, and one Postgres instance to operate.
  • Serving tenant-specific views at different hostnames or subfolders (e.g. acme.example.com vs example.com/acme) from the same Django deployment.
  • Bulk-provisioning new tenants quickly by cloning a template schema rather than re-running the full migration set for each signup.

Under The Hood

Architecture django-tenants layers itself into three points of the Django request/ORM lifecycle: a custom DatabaseWrapper (django_tenants/postgresql_backend/base.py) that manages the PostgreSQL search_path per connection, middleware (django_tenants/middleware/main.py, subfolder.py) that resolves a tenant from the request and calls connection.set_tenant(), and a TenantSyncRouter (django_tenants/routers.py) that Django’s migration framework consults via allow_migrate to decide whether an app belongs in the shared public schema or in each tenant schema. TenantMixin.save() (django_tenants/models.py) hooks schema creation into normal model save, guarding against a tenant creating or modifying a schema outside its own or the public one. Multi-schema migrations are dispatched through django_tenants/migration_executors/ with three interchangeable executors (standard, multiprocessing via multiproc.py, subprocess via subproc.py), letting large tenant counts migrate in parallel; what breaks if this core abstraction changes is any assumption that search_path correctly scopes a query, since every model, cache key, and content-type lookup depends on it being set before that connection is used.

Tech Stack The project targets Django 5.2 through 6.1 and Python 3.10+, declared in pyproject.toml with django>=5.2,<6.2 as its only runtime dependency; it supports both psycopg2 and psycopg3 via django.db.backends.postgresql.psycopg_any.is_psycopg3. Dev dependencies (coverage, gunicorn, mypy, psycopg) are isolated in an optional-dependencies.dev group. A large embedded PL/pgSQL routine in django_tenants/clone.py (ported from the pg-clone-schema project) does schema-to-schema cloning of tables, sequences, views, functions, and data directly in Postgres. The package ships its own mypy plugin (django_tenants/mypy_plugin.py) that injects a tenant attribute onto Django’s HttpRequest type so static type-checkers understand the attribute the middleware adds at runtime.

Code Quality The test suite spans 26 files under django_tenants/tests/ covering routers, middleware, migration executors, multi-type tenants, subfolder routing, caching, static/file storage, template loaders, and the mypy plugin itself — exercised against a real PostgreSQL instance via run_tests.sh or Docker Compose. CI runs two separate matrices on GitHub Actions: a Postgres-compatibility matrix (versions 14 through 17) and a Django/Python/psycopg compatibility matrix, so version-support claims are continuously verified rather than asserted. Error handling favors explicit, descriptive exceptions (e.g. raising when a tenant tries to save or delete a schema outside its own or the public one) over silent failures, and recent history shows targeted bug fixes referencing specific issue numbers (case-insensitive schema-name collisions, psycopg3 schema-existence checks) with accompanying comments explaining the reasoning.

What Makes It Unique Unlike row-level multi-tenancy libraries that add a tenant_id foreign key to every table, django-tenants uses PostgreSQL’s native schema namespacing so tenant isolation is enforced by the database itself, and existing querysets, migrations, and admin code need no tenant-filtering logic since the search_path swap happens beneath the ORM. Its schema-cloning path is the more distinctive piece: rather than replaying a growing migration history for every new tenant, it can copy a fully-migrated template schema’s DDL and data directly, which keeps tenant provisioning time roughly constant as the schema’s migration history grows. The pluggable multiprocessing/subprocess migration executors are also a direct response to a problem specific to schema-per-tenant systems — running migrate sequentially against hundreds or thousands of schemas — that row-based multi-tenancy approaches never encounter.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search