guide

Multi-Tenant SaaS Architecture with Django

Build multi-tenant SaaS with Django using shared database, schema-per-tenant, or database-per-tenant. Complete guide with django-tenants, Row-Level Security, and data isolation patterns. From EUR 8,000.

TL;DR

Multi-tenant architecture is the foundation decision for every Django SaaS application. The three strategies — shared database with row-level isolation, schema-per-tenant (PostgreSQL schemas), and database-per-tenant — each have distinct cost, complexity, and compliance trade-offs. In production, shared-database with django-tenants or django-multitenant handles 90% of B2B SaaS products at EUR 8,000-15,000 development cost, while schema-per-tenant is required for regulated industries needing strict data isolation.

Three Tenancy Models: Architecture, Cost, and When to Use Each

Every SaaS application must isolate customer data while sharing infrastructure efficiently. Django supports three multi-tenancy strategies, each with different trade-offs for data isolation, operational complexity, and per-tenant cost.

1. Shared Database with Row-Level Security (most common)

  • All tenants share the same database and the same tables. Every row includes a tenant_id foreign key. Queries are automatically filtered by tenant using middleware or model managers.
  • Django packages: django-multitenant (Citus Data), django-scopes, or custom middleware with threading.local()
  • Database setup: Standard PostgreSQL with indexes on tenant_id. Enable PostgreSQL Row-Level Security (RLS) policies for defence-in-depth: CREATE POLICY tenant_isolation ON app_table USING (tenant_id = current_setting('app.current_tenant')::int);
  • Pros: Simplest to implement, lowest infrastructure cost (one database for all tenants), easy migrations (one schema), works with Django ORM out of the box
  • Cons: Noisy-neighbour risk (one tenant's heavy query affects all), harder to comply with data residency requirements, backup/restore is all-or-nothing
  • Best for: B2B SaaS with fewer than 10,000 tenants, SMB-focused products, MVPs. Development cost: EUR 8,000-12,000.

2. Schema-Per-Tenant (PostgreSQL schemas)

  • Each tenant gets their own PostgreSQL schema within a single database. The public schema holds shared tables (users, plans, billing). Tenant schemas hold application data.
  • Django package: django-tenants (the most mature option, 2,800+ GitHub stars). Middleware sets search_path per request based on subdomain or header.
  • Database setup: CREATE SCHEMA tenant_acme; SET search_path TO tenant_acme, public; Django migrations run per-schema automatically via python manage.py migrate_schemas.
  • Pros: Strong data isolation without separate databases, per-tenant backup/restore possible, tenant-specific customisations via schema differences
  • Cons: Migration complexity grows with tenant count (1,000 tenants = 1,000 schema migrations), connection pooling requires schema-aware configuration (PgBouncer with search_path hooks), max ~5,000 schemas before PostgreSQL catalog performance degrades
  • Best for: B2B SaaS with 50-5,000 tenants, products requiring data isolation for compliance (GDPR, HIPAA), enterprise customers demanding logical separation. Development cost: EUR 10,000-18,000.

3. Database-Per-Tenant

  • Each tenant gets a fully separate PostgreSQL database. Django's database router directs queries to the correct database.
  • Django setup: Dynamic DATABASES configuration via custom database router. Use django-dynamic-db-router or implement class TenantRouter with db_for_read() and db_for_write() methods.
  • Pros: Complete isolation (compliance-friendly), independent scaling per tenant, easy per-tenant backup/restore/deletion, no noisy-neighbour issues
  • Cons: Highest infrastructure cost (separate database per tenant), operational complexity (connection management, monitoring), cross-tenant queries require federation
  • Best for: Enterprise SaaS with fewer than 200 large tenants, healthcare/fintech with strict compliance, tenants requiring dedicated resources. Development cost: EUR 15,000-25,000.

Implementing Multi-Tenancy with django-tenants: Step-by-Step

django-tenants is the production-proven package for schema-per-tenant multi-tenancy in Django. It has been used in SaaS products handling 3,000+ tenants on a single PostgreSQL instance. Here is the complete implementation pattern.

Installation and configuration:

  • Install: pip install django-tenants. Requires PostgreSQL (no MySQL/SQLite support — schemas are a PostgreSQL feature).
  • Set DATABASE_ENGINE to django_tenants.postgresql_backend in settings.
  • Configure TENANT_MODEL = "customers.Client" and TENANT_DOMAIN_MODEL = "customers.Domain" in settings.
  • Split INSTALLED_APPS into SHARED_APPS (auth, billing, admin — live in public schema) and TENANT_APPS (your application models — replicated per tenant schema).

Tenant model design:

  • Create a Client model extending TenantMixin: includes schema_name, name, paid_until, on_trial, created_on fields.
  • Create a Domain model extending DomainMixin: maps domains/subdomains to tenants. Supports custom domains: acme.yourapp.com or app.acme.com.

Middleware and request routing:

  • Add django_tenants.middleware.main.TenantMainMiddleware as the first middleware. It reads the Host header, looks up the tenant, and sets search_path on the database connection.
  • For API-based tenancy (JWT token contains tenant ID instead of subdomain), implement custom TenantMiddleware that extracts tenant from authentication headers.

Migration workflow:

  • python manage.py migrate_schemas --shared — migrates the public schema only
  • python manage.py migrate_schemas --tenant — migrates all tenant schemas
  • python manage.py migrate_schemas --schema=tenant_acme — migrate a single tenant (useful for rolling deployments)
  • For 1,000+ tenants, run migrations in batches using --executor=parallel with worker count matching available CPU cores.

Performance configuration for production:

  • Connection pooling: PgBouncer in transaction mode with server_reset_query = "DISCARD ALL" to clear search_path between connections
  • Indexes: Ensure all tenant-schema tables have indexes on frequently queried fields. Each schema has independent indexes.
  • Monitoring: Track schema count, per-tenant query performance, and migration duration. Alert when schema count exceeds 3,000 (performance cliff).
  • Caching: Use tenant-prefixed cache keys — cache.set(f"tenant_{tenant.pk}:dashboard_data", data) — to prevent cross-tenant cache pollution.

Data Isolation and Security Patterns

Data leakage between tenants is the most critical security risk in multi-tenant SaaS. A single bug that exposes one tenant's data to another can destroy trust and violate GDPR. Here are the defence-in-depth patterns every Django SaaS must implement.

Application-level isolation (first layer):

  • Custom model manager: Override get_queryset() on all tenant-scoped models to automatically filter by current tenant. Use django-scopes which raises an exception if a queryset is evaluated without an active tenant scope — preventing accidental data leakage in background tasks.
  • Middleware injection: Store current tenant in threading.local() or contextvars.ContextVar (Python 3.7+, async-safe). Every model manager reads from this context.
  • Admin protection: Override get_queryset() in all ModelAdmin classes to filter by tenant. Without this, Django admin exposes all tenants' data to any admin user.
  • API serializer validation: In DRF serializers, validate that referenced objects (foreign keys) belong to the current tenant. Prevent IDOR attacks where tenant A references tenant B's object by ID.

Database-level isolation (second layer):

  • PostgreSQL Row-Level Security: Even with application-level filtering, enable RLS as a safety net. Create policies per table: ALTER TABLE orders ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_access ON orders USING (tenant_id = current_setting('app.current_tenant')::int);
  • Connection-level tenant setting: Set SET app.current_tenant = %s on each database connection via Django's connection.cursor().execute() in middleware. RLS policies reference this setting.
  • Separate database credentials: For high-security deployments, create PostgreSQL roles per tenant with GRANT SELECT, INSERT, UPDATE, DELETE ON schema_name.* TO tenant_role;

File storage isolation (third layer):

  • S3 bucket structure: s3://your-bucket/tenants/{tenant_id}/uploads/. IAM policies restrict access per prefix.
  • Django storage backend: Custom FileSystemStorage or S3Boto3Storage subclass that prepends tenant ID to all file paths.
  • Signed URLs: Generate pre-signed S3 URLs with tenant-specific paths. URLs expire after 15 minutes.

Testing for data isolation:

  • Write pytest fixtures that create two tenants with identical data structures. Run every view test twice — once per tenant — and assert zero cross-tenant data leakage.
  • Use factory_boy with tenant-aware factories: class OrderFactory(factory.django.DjangoModelFactory): tenant = factory.LazyAttribute(lambda o: get_current_tenant())
  • Automated security scan: Nightly CI job that attempts cross-tenant API access for every endpoint. Any 200 response with another tenant's data fails the build.

Tenant Onboarding and Automated Provisioning

Self-service tenant provisioning — from signup to fully functional account in under 30 seconds — is essential for product-led growth SaaS. Here is the complete onboarding pipeline.

Signup flow architecture:

  1. Registration (public schema): User submits email, password, company name. Create User and Client records in the public/shared schema. Generate a unique schema_name from company slug (sanitised, max 63 characters for PostgreSQL identifier limit).
  2. Schema provisioning: Call tenant.create_schema(check_if_exists=True) from django-tenants. This runs CREATE SCHEMA and applies all tenant migrations. For a typical SaaS with 30-40 models, this takes 2-5 seconds.
  3. Seed data: Populate default categories, settings, templates, and sample data. Use a post_schema_sync signal handler or a provision_tenant() Celery task.
  4. DNS/subdomain: Create Domain record mapping acme.yourapp.com to the new tenant. If using wildcard DNS (*.yourapp.com pointing to your server), no DNS propagation wait is needed.
  5. Welcome email: Celery task sends onboarding email with login link, getting-started guide, and calendar link for optional demo call.

Custom domain support:

  • Tenant adds their custom domain (e.g., app.acme.com) via settings page.
  • Backend verifies domain ownership via DNS TXT record check: _verification.app.acme.com TXT "yourapp-verify=abc123"
  • Once verified, provision SSL certificate via Let's Encrypt / Caddy automatic HTTPS.
  • Add Domain record with is_primary=True. Old subdomain continues working as redirect.
  • Implementation cost: EUR 1,000-2,000 on top of base multi-tenancy.

Tenant lifecycle management:

  • Trial expiry: Celery Beat scheduled task checks paid_until daily. Send reminder emails at 7, 3, and 1 day before trial ends. On expiry, set tenant to read-only mode (middleware returns 402 for write operations).
  • Suspension: Delinquent accounts get suspended — middleware returns a "please update payment" page for all requests. Data preserved for 90 days.
  • Deletion: GDPR-compliant tenant deletion: DROP SCHEMA tenant_acme CASCADE; + delete S3 prefix + anonymise shared-schema records. Implement as async Celery task with confirmation email.
  • Data export: Before deletion, offer full data export in JSON/CSV format. Use django-import-export or custom management command that serialises all tenant-schema data.

Provisioning at scale:

  • For SaaS products onboarding 50+ tenants per day, pre-provision schema pools: maintain 10 empty schemas ready to assign, replenished by a background task. This reduces signup latency from 5 seconds to under 500ms.
  • Monitor provisioning success rate and duration. Alert on failures — a stuck migration blocks the entire signup pipeline.

Development Packages and Infrastructure Costs

Shared Database Multi-Tenancy (EUR 8,000-12,000):

  • Row-level tenant isolation with django-multitenant or custom middleware
  • PostgreSQL RLS policies as defence-in-depth
  • Tenant-aware model managers, admin, and DRF serializers
  • Self-service signup with automated provisioning
  • Tenant settings and branding (logo, colours)
  • Data export per tenant (GDPR compliance)
  • Timeline: 6-8 weeks

Schema-Per-Tenant Architecture (EUR 12,000-18,000):

  • Full django-tenants implementation with schema isolation
  • Subdomain routing + custom domain support with SSL
  • Shared/tenant app splitting with optimised migrations
  • PgBouncer connection pooling configuration
  • Tenant lifecycle management (trial, suspension, deletion)
  • Background task tenant isolation (Celery tenant-aware tasks)
  • Timeline: 8-12 weeks

Enterprise Database-Per-Tenant (EUR 18,000-25,000):

  • Dedicated PostgreSQL databases per tenant
  • Dynamic database routing with connection management
  • Per-tenant scaling and resource allocation
  • Cross-tenant analytics via read replicas or data warehouse
  • Compliance documentation (SOC 2, GDPR data processing records)
  • Timeline: 12-16 weeks

Monthly infrastructure costs (typical):

  • 10 tenants: Single VPS EUR 40-80/month (4 vCPU, 8GB RAM, managed PostgreSQL)
  • 100 tenants: EUR 150-300/month (dedicated database server + app server)
  • 1,000 tenants: EUR 400-800/month (load-balanced app servers + RDS Multi-AZ)
  • 5,000+ tenants: EUR 1,200-3,000/month (auto-scaling, read replicas, CDN, monitoring)

Technology stack recommendation: Django 5.x + PostgreSQL 16 + Redis (caching + Celery broker) + Celery (async tasks) + PgBouncer (connection pooling) + S3-compatible storage (Minio or AWS S3) + Caddy or Nginx (reverse proxy + auto-SSL for custom domains).

Frequently Asked Questions

Which multi-tenancy model should I choose for my Django SaaS?

For 90% of B2B SaaS products, shared-database with row-level isolation is the right starting point. It is the simplest to build, cheapest to operate, and handles up to 10,000 tenants on a single PostgreSQL instance. Choose schema-per-tenant if your customers require logical data separation for compliance (healthcare, finance, government) or if you need per-tenant backup/restore. Choose database-per-tenant only for enterprise SaaS with fewer than 200 large tenants requiring dedicated resources and strict regulatory compliance.

Can I migrate from shared-database to schema-per-tenant later?

Yes, but it is a significant effort — typically 4-8 weeks of development and careful data migration. The migration involves creating per-tenant schemas, copying data with tenant filtering, updating all application code to use django-tenants patterns, and running parallel systems during transition. It is cheaper to start with the right model. If there is any chance you will need schema isolation within 12 months, start with django-tenants from day one.

How many tenants can a single PostgreSQL instance handle?

For shared-database: 10,000+ tenants comfortably on a well-indexed PostgreSQL 16 instance (16 vCPU, 64GB RAM). For schema-per-tenant: practical limit is 3,000-5,000 schemas before PostgreSQL catalog queries slow down. Beyond that, shard across multiple database instances. For database-per-tenant: limited by connection management — typically 100-200 databases per PostgreSQL cluster with PgBouncer.

Build Your Multi-Tenant SaaS Architecture

Describe your SaaS product, expected tenant count, and compliance requirements. I will recommend the right tenancy model and provide a fixed-price development quote.

Get a Multi-Tenancy Architecture Quote

or message directly: Telegram · Email