Django Multi-Tenant SaaS Production Checklist
A production checklist for multi-tenant Django SaaS apps: choose shared tables, schemas, or databases, enforce tenant isolation in queries and caches, scope files and background jobs, handle custom domains, run migrations per tenant, and test for cross-tenant leaks.
Problem statement
In a multi-tenant SaaS, one Django deployment serves many customers. The worst possible bug is one customer seeing another's data, and it can come from a missing filter in a view, a cache key without the tenant, a Celery task that forgets its tenant context, or a file URL that is guessable. Tenancy also affects migrations, performance, backups, and custom domains. This checklist covers isolation and operations for the common architectures.
- Pick an architecture deliberately: shared tables with a
tenant_id, schema per tenant (django-tenants), or database per tenant. - Enforce tenant scoping centrally (custom managers, middleware, row-level security), not in each view.
- Include the tenant in cache keys, file paths, Celery task arguments, and log context.
- Automate per-tenant migrations, provisioning, and custom domain TLS.
- Write automated cross-tenant tests and include tenant isolation in penetration tests.
Checklist
Architecture
- Isolation model chosen and documented: shared tables (
tenant_idcolumn), separate schemas, or separate databases. - Tenant resolution defined: subdomain, custom domain, header, or user membership, in one middleware.
- Superuser and support access paths explicit and audited.
- Plan for large tenants (dedicated database or shard) without rewriting the app.
Data isolation
- Shared tables: every tenant-owned model has a non-null
tenantforeign key, indexed, and default managers filter by the current tenant. - Unique constraints include the tenant (
UniqueConstraint(fields=["tenant", "slug"])). - PostgreSQL row-level security as a backstop for shared tables, with the tenant set per connection.
- Schema-per-tenant:
search_pathset per request (django-tenants), shared vs tenant apps configured correctly. - Admin, exports, and APIs scoped; querysets built from
request.tenant, never from user-provided IDs alone.
Side effects and caches
- Cache keys include the tenant (
KEY_FUNCTIONor a prefix per tenant); cached pages vary by host. - File storage paths prefixed by tenant; private files served only after tenant and permission checks.
- Celery tasks receive the tenant ID explicitly and set tenant context at the start; no reliance on thread locals from the request.
- Emails, webhooks, and notifications use tenant-specific settings and branding.
Operations
- Migrations run for all tenants in deploys, with progress and failure handling (schema or database per tenant).
- Tenant provisioning and deletion automated, including data export and secure deletion.
- Custom domains: DNS verification, on-demand TLS (Caddy, cert-manager), and
ALLOWED_HOSTShandled dynamically. - Per-tenant rate limits and resource quotas to prevent noisy neighbors.
- Monitoring and logs tagged with tenant ID.
Testing and assurance
- Automated tests that create two tenants and assert that every API endpoint and view rejects cross-tenant access.
- Pen test scope includes tenant isolation with accounts in at least two tenants.
- Audit logs of support staff accessing tenant data.
Explanation
Tenancy is a cross-cutting concern: it must apply wherever data is read, written, cached, queued, or stored. Shared-table designs rely on the application (and optionally row-level security) to filter correctly; schema and database designs push isolation into PostgreSQL at the cost of more complex migrations and operations. Whatever the model, central enforcement and explicit tenant context in asynchronous work are what prevent leaks, and two-tenant tests make the guarantee checkable.
Edge cases / notes
- Thread locals. Storing the tenant in thread locals works for sync requests but breaks in async code and Celery; pass it explicitly or use
contextvars. - Search indexes. External search engines (Elasticsearch, Meilisearch) need tenant filters or per-tenant indexes too.
- Analytics. Cross-tenant reporting needs a separate, read-only path with clear access controls.
Next steps
- Implement schemas with Deploy Django Multi-Tenant With django-tenants.
- Add customer domains with Custom Domains per Tenant With Django and Caddy.
- Prepare launch with the Django SaaS Launch Checklist.
FAQ
Which multi-tenant model should a new Django SaaS use?
Shared tables with a tenant foreign key suit most new products; schemas or databases suit stricter isolation or large customers.
How do I prevent forgetting the tenant filter?
Use default managers or querysets scoped to the current tenant, and back them with tests and optionally row-level security.
How should Celery tasks know their tenant?
Pass the tenant ID as an argument and set the tenant context at the start of the task.
How do I test tenant isolation?
Create two tenants in tests and assert that every endpoint denies access to the other tenant's objects.