Technical
Django Models for Multi-Tenant SaaS
Every SaaS product eventually needs multi-tenancy. One database, many customers, strict data isolation. Get this wrong and you have a lawsuit waiting. Get it right and you have a product that scales to hundreds of tenants on a single database. Here is the Django model pattern I use.
The Simple Approach: Tenant Foreign Key
Every model gets a tenant foreign key. Every query filters by tenant. That sentence sounds trivial. It is not. Missing one filter leaks data across customers.
class Tenant(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Post(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
body = models.TextField()
class Meta:
indexes = [models.Index(fields=['tenant', 'title'])]Notice the composite index. Every query filters on tenant first, then the real column. Your indexes should match that pattern.
The Manager Pattern
Relying on developers to remember the tenant filter is a bug factory. I use a tenant-aware manager:
class TenantManager(models.Manager):
def for_tenant(self, tenant):
return self.get_queryset().filter(tenant=tenant)Every view uses Post.objects.for_tenant(request.tenant). If someone writes Post.objects.all(), code review catches it.
Middleware for Tenant Resolution
Tenant is usually resolved from the subdomain or a header. A middleware reads the request and attaches the tenant to request.tenant. Every view inherits the right tenant automatically.
Why Not Schema-per-Tenant
Schema-per-tenant (each tenant gets its own Postgres schema) sounds cleaner. In practice it multiplies migration complexity by the number of tenants. At 100 tenants, deploying a schema change becomes a multi-hour job. I stick with foreign-key tenancy until performance forces me to split.
The Testing Hack
Write one test that creates two tenants, inserts data for each, and verifies tenant A cannot see tenant B's data. Run that test on every commit. It catches data leaks before production.
When to Consider Separate Databases
If a single tenant needs so much data that their queries slow down everyone else, separate that tenant into their own database. This is rare. Most SaaS apps never hit this threshold.
Soft Deletes Are Tenant-Scoped Too
If you soft-delete records, the deleted flag is still a per-tenant concern. A default manager that hides soft-deleted rows must also respect the tenant filter. I stack these as chained queryset methods: Post.objects.for_tenant(t).active().
Handling Tenant Deletion
When a tenant leaves, the on_delete=models.CASCADE on the foreign key removes their data automatically. I add a soft-delete tombstone first so we can recover within 30 days if they change their mind. The real hard delete runs as a scheduled job.
Observability
Every log line in a multi-tenant app should include the tenant ID. I add a middleware that attaches tenant context to the logger, so every subsequent log line carries it. When a bug surfaces, I can filter the logs to that tenant alone and see what happened.
See the Django documentation on custom managers for the pattern that makes multi-tenant queries safe by default.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read