Designing a Multi-Tenant SaaS Architecture from Scratch
Three models, one decision that's hard to reverse
Multi-tenancy comes down to three shapes: a shared database with a tenantId column on every table, a shared database with a separate schema per tenant, or a fully separate database per tenant. Most teams default to the first because it's the easiest to build, and for most SaaS products it's also the right call — but it's worth understanding what you're trading away before committing, because migrating between models later means touching every query in the system.
Shared database, tenant-scoped rows
Every table gets a tenantId, every query gets a WHERE tenantId = ?, and — critically — that filter has to be enforced at a layer that can't be forgotten. I've seen this go wrong exactly one way in production: someone writes a new query, in a hurry, and forgets the tenant filter. The fix isn't discipline, it's making the mistake impossible:
// Mongoose middleware — every query on this model gets tenant-scoped automatically
schema.pre(/^find/, function () {
if (!this.getQuery().tenantId) {
this.where({ tenantId: currentTenantContext() });
}
});
This model wins on cost (one database to operate, one connection pool, one set of backups) and on cross-tenant analytics (aggregate queries across all tenants are trivial). It loses on the blast radius of a bug — a missing tenant filter is a data leak between customers, not a crashed query — and on "noisy neighbor" problems, where one tenant's heavy usage can degrade performance for everyone sharing the database.
Schema-per-tenant and database-per-tenant
Separate schemas give you a real permission boundary — a bug can't accidentally query across the wall the way a forgotten WHERE clause can — at the cost of running migrations against N schemas instead of one, and connection pooling that has to be tenant-aware. Full database-per-tenant takes that further: true resource isolation, the ability to place a specific enterprise customer's data in a specific region for compliance, and a genuinely simple story for "delete everything for this tenant." It also means N databases to back up, monitor, and patch, which stops being fun somewhere around tenant 200 unless you invest early in provisioning automation.
What actually decides it
- Compliance requirements. If any customer needs data residency guarantees or is willing to pay for dedicated infrastructure, database-per-tenant for that tier isn't optional.
- Tenant count and size distribution. Thousands of small tenants favor shared; dozens of large enterprise tenants favor isolation.
- Team size. Schema-per-tenant migrations at 3am with a five-person team is a real operational tax — don't take it on before you need to.
My default starting point is shared database with enforced tenant-scoping and a design that keeps the door open to moving specific large tenants to isolated databases later, without a full rewrite — usually by keeping all data access behind a repository layer that doesn't care which model is underneath it.