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.

Multi-tenant DjangoResolve tenantdomain, header, userScope dataqueries, schema, DBScope side effectscache, files, tasksTest isolationcross-tenant checksEvery layer that stores or reads data must carry the tenant with it.
  1. Pick an architecture deliberately: shared tables with a tenant_id, schema per tenant (django-tenants), or database per tenant.
  2. Enforce tenant scoping centrally (custom managers, middleware, row-level security), not in each view.
  3. Include the tenant in cache keys, file paths, Celery task arguments, and log context.
  4. Automate per-tenant migrations, provisioning, and custom domain TLS.
  5. Write automated cross-tenant tests and include tenant isolation in penetration tests.

Checklist

Architecture

  • Isolation model chosen and documented: shared tables (tenant_id column), 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 tenant foreign 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_path set 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.
model and trade-offsShared tablesSchema per tenantDB per tenantIsolation strengthapp-enforcedsearch_pathstrongMigrationsonceper schemaper databaseTenants supportedvery manyhundreds to thousandstens to hundredsPer-tenant backuphardpg_dump -neasyCross-tenant reportseasyharderhardestStronger isolation costs operational effort and scaledjango-deployment.com
Many SaaS products start with shared tables and move large customers out later.

Side effects and caches

  • Cache keys include the tenant (KEY_FUNCTION or 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_HOSTS handled 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.
leak path and guardLeak pathGuardview filters by ID onlyscoped managers from request.tenantcache key without tenanttenant in KEY_FUNCTION or prefixtask runs in wrong tenantpass tenant ID, set contextguessable file URLstenant paths, signed URLsIsolation must be enforced in every layer, not only queriesdjango-deployment.com
Automated two-tenant tests catch most of these before release.

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

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.

Keep going