Skip to content

Multi-Tenancy

The system uses django-pgschemas to implement schema-per-tenant isolation in PostgreSQL. Each tenant gets a dedicated PostgreSQL schema, providing complete data isolation at the database level.

Three Schema Types

Schema Type Apps Purpose
public Shared django_pgschemas, apps.tenants, contenttypes Tenant registry, domain routing, content types
main Static tenant Django contrib, third-party, apps.main, apps.users LMS home site (admin panel, allauth, central user management)
tenant_{slug} Dynamic tenant Django contrib, third-party, apps.users, all LMS apps Per-tenant business data (loans, borrowers, payments, etc.)

App Distribution

App Category Public Main Tenant
django_pgschemas Yes
apps.tenants Yes
contenttypes Yes
Django contrib (auth, sessions, etc.) Yes Yes
Third-party (allauth, guardian, pghistory) Yes Yes
apps.main Yes
apps.users Yes Yes
All LMS business apps Yes

Note

apps.users exists in both main and tenant_{slug} schemas, giving each tenant its own user table. Users in the main schema are for LMS administration; users in tenant schemas are for tenant-specific operations.

Schema Routing

Domain Resolution

  • main schema resolves via FALLBACK_DOMAINS (e.g., localhost, app.lendsmart.io)
  • tenant_{slug} schemas resolve via subdomain (e.g., acme.app.lendsmart.io)
  • DomainRoutingMiddleware inspects the Host header and activates the correct PostgreSQL schema via SET search_path

Database Router

DATABASE_ROUTERS = ("django_pgschemas.routers.TenantAppsRouter",)

Warning

The database router is required. It ensures shared-app queries (User, Tenant, ContentType) always route to the correct schema. Without it, shared-app tables get created in tenant schemas, causing FK violations and data isolation bugs.

Tenant Model

The Tenant model in apps.tenants lives in the public schema:

  • name --- Display name
  • slug --- URL-safe identifier, used as schema suffix (tenant_{slug})
  • status --- active, suspended, or terminated
  • locale --- Default locale for the tenant (e.g., en-US, es-MX)
  • timezone --- Default timezone (e.g., America/New_York)
  • default_currency --- ISO 4217 currency code (e.g., USD)
  • features --- JSONB field for feature flags

The Domain model maps domains/subdomains to tenants for routing.

User Model

Dual-Schema Users

apps.users is installed in both the main and tenant_{slug} schemas, resulting in separate users_user tables per schema:

  • Main schema users --- LMS administrators who manage the platform
  • Tenant schema users --- Staff who work within a specific tenant (loan officers, collectors, admins)

Tenant Roles

Users in tenant schemas have a tenant_role field with these values:

Role Description
superadmin Full access to everything in the tenant
admin Configuration and financial management
loan_officer Loan lifecycle and borrower management
collector Collection actions and promise-to-pay
viewer Read-only access

Schema-Aware Management Commands

django-pgschemas provides commands that run across schemas:

Command Purpose
migrateschema Run migrations across schemas
runschema Run any management command across schemas
whowill Preview which schemas a command would target
createrefschema Create reference schema for cloning

Schema Selectors

Schema selectors control which schemas a command targets:

Selector Targets
-s <name> Specific schema(s) by name, tenant key, or domain
-as All schemas (public + static + dynamic)
-ss Static schemas only (public, main)
-ds Dynamic schemas only (tenant_*)
-ts Tenant-like schemas (static + dynamic, excludes public)
-x <name> Exclude specific schema(s)
--parallel Run across schemas in parallel threads

Common Operations

# Migrate all schemas after model changes
uv run python manage.py migrateschema -as

# Migrate only dynamic tenant schemas
uv run python manage.py migrateschema -ds

# Run a command on a specific tenant
uv run python manage.py runschema loaddata seed.json -s tenant_acme

# Preview which schemas will be targeted
uv run python manage.py whowill -ds

Creating a New Tenant

Tenants are created programmatically. When a Tenant instance is saved with auto_create_schema=True (the default), django-pgschemas automatically:

  1. Creates the PostgreSQL schema (tenant_{slug})
  2. Runs all migrations for tenant-app models
  3. The tenant is accessible via its configured domain

Tenant Status Enforcement

The TenantStatusMiddleware checks tenant status on every request:

  • active --- Normal operation
  • suspended --- Returns HTTP 503 (Service Unavailable)
  • terminated --- Returns HTTP 503 (Service Unavailable)

Common Pitfalls

Never hardcode schema names

Always use django-pgschemas context managers and middleware for schema activation. Never manually set search_path.

Cross-schema queries are not supported

Data in one tenant schema cannot directly reference data in another tenant schema. Use the public schema for shared lookups.

ContentType in public schema only

django.contrib.contenttypes should only be in the public schema's APPS list. Tenant schemas access the django_content_type table via PostgreSQL search_path.

PGSCHEMAS_EXTRA_SEARCH_PATHS cannot include 'public'

The library raises ImproperlyConfigured if you add "public" to extra search paths --- it's always in the search path automatically.

See Also