Designing Multi-Tenant SaaS That Scales
Choosing an isolation model, keeping tenant data separate, and dodging the noisy-neighbour and migration traps that bite SaaS later.
The decision that shapes everything: how you isolate tenants
Multi-tenant SaaS lives or dies on one early choice — how strictly you isolate one customer's data from another. Get it wrong and you inherit a problem that compounds with every tenant you add. The three common models are a shared schema with a tenant_id column on every row, a schema-per-tenant approach inside one database, and a database-per-tenant approach. None is universally correct; the right answer depends on your customers, your scale, and your compliance obligations.
For most early-stage products, a shared schema with a tenant discriminator is the pragmatic default. It is cheap to operate, trivial to migrate, and supports thousands of small tenants on modest hardware. The cost is that isolation now lives entirely in your application code — and a single missing WHERE clause leaks one customer's data to another. A database-per-tenant model flips that trade: isolation is physical and bulletproof, but every operational task multiplies by the number of tenants. Schema-per-tenant sits in between, sharing a database server while keeping each tenant's tables apart. The trick is to choose with eyes open rather than drifting into whichever model your first prototype happened to use.
Make tenant isolation impossible to forget
Do not rely on developers remembering to filter by tenant on every query. Enforce it at a lower layer so a mistake fails closed instead of leaking data.
- Row-level security in PostgreSQL ties every query to a session variable holding the current tenant. Even a query that forgets the filter returns nothing for the wrong tenant.
- A repository layer that injects the tenant predicate centrally, so feature code physically cannot issue an unscoped query.
- Connection-level context set from the authenticated request, never from user-supplied input in the query body.
The principle is defence in depth: application code, the data-access layer, and the database all agree on who the caller is. A bug in one layer should not be enough to cross a tenant boundary. It is worth writing an automated test that deliberately tries to read another tenant's data and asserts that it fails — a regression here is the single most damaging bug a multi-tenant product can ship, and it deserves a test that guards it forever.
The noisy-neighbour problem is a capacity problem
In a shared model, one tenant running a heavy report can degrade everyone else. Plan for this before it happens in production at 2 a.m.
- Put per-tenant rate limits on expensive endpoints, not just global ones.
- Move long-running work — exports, bulk imports, analytics — onto a queue with per-tenant concurrency caps.
- Track resource use per tenant so you can see which customer is driving load before it becomes an incident.
- Set query timeouts so one runaway report cannot hold a connection open and starve everyone else.
When a single large customer consistently dwarfs the rest, that is your signal to graduate them to a dedicated schema or database. A good architecture lets you move one tenant without re-platforming the other thousand. The ability to relocate a heavy tenant onto its own infrastructure — while the long tail stays happily shared — is what keeps a single noisy customer from forcing you to over-provision for everyone.
Migrations and onboarding are where shared schemas earn their keep
A schema change in a shared model is one migration. In a database-per-tenant model it is one migration multiplied by every tenant, run in a loop with careful failure handling and a way to resume halfway through. That operational difference is easy to underestimate during design and painful to discover during a release, when migration number four hundred fails and you have to work out which tenants are now half-migrated. If you expect frequent schema evolution and many small tenants, the shared model keeps you fast. If you have a few large enterprise customers who demand physical separation, the per-database model is worth its operational weight.
Onboarding follows the same logic. Spinning up a new tenant in a shared schema is a row insert. In a per-database model it is provisioning, migrating, and seeding a fresh database — fine if you automate it, but it has to be automated from day one, not bolted on after your tenth signup. The moment onboarding becomes a manual checklist, your growth is capped by how fast a human can click through it.
Design for the day a tenant needs to leave
Two requirements always arrive eventually: export everything for one tenant, and delete everything for one tenant. Both are far easier when isolation is clean. Build a per-tenant export and a hard-delete path early, while the data model is small. Retrofitting them across dozens of tables after launch — especially with foreign keys and soft-delete flags scattered around — is the kind of work nobody enjoys and auditors always ask about. Customers increasingly write data-portability and deletion guarantees into their contracts, so treating these as core features rather than someday-tasks pays off the first time a deal hinges on them.
Pick the model your customers actually need
The honest summary: most products should start with a shared schema and rigorous, enforced isolation, because it is the fastest to build and operate. Move to stronger isolation when a real requirement demands it — a large customer who needs their data physically separate, a regulated industry, or a tenant whose load justifies dedicated capacity. The architectures are not mutually exclusive; a mature product often runs most tenants shared and a handful on dedicated infrastructure, which is exactly the flexibility you want to design for from the start.
How BSH can help
At BSH Technologies we have built and operated multi-tenant SaaS for clients well beyond our base in Thrissur, and we treat tenant isolation as a first-class architectural concern, not an afterthought. If you are planning a new product or feeling the strain of a model that no longer fits, we can review your isolation strategy, harden your data-access layer, and map a migration path that does not require a rewrite. Reach out and we will talk through what actually fits your customers.
From the blog
View all postsHitting Green Core Web Vitals in Next.js
A practical guide to LCP, INP and CLS in Next.js — image handling, font loading, the App Router boundary, and costly third-party scripts.
Infrastructure as Code: A Terraform Starting Point
A grounded intro to Terraform — remote state, modules, plan-before-apply discipline, and the early mistakes that turn IaC into a liability.