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¶
mainschema resolves viaFALLBACK_DOMAINS(e.g.,localhost,app.lendsmart.io)tenant_{slug}schemas resolve via subdomain (e.g.,acme.app.lendsmart.io)DomainRoutingMiddlewareinspects theHostheader and activates the correct PostgreSQL schema viaSET search_path
Database Router¶
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 nameslug--- URL-safe identifier, used as schema suffix (tenant_{slug})status---active,suspended, orterminatedlocale--- 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:
- Creates the PostgreSQL schema (
tenant_{slug}) - Runs all migrations for tenant-app models
- The tenant is accessible via its configured domain
Tenant Status Enforcement¶
The TenantStatusMiddleware checks tenant status on every request:
active--- Normal operationsuspended--- 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¶
- Architecture Overview --- High-level system design
- Layered Architecture --- How apps are structured within a tenant
- Project Structure --- Directory layout and app organization