The multi-tenant SaaS checklist: seven decisions, what each costs if you defer it, and the one everyone worries about far too early.
Overview
| # | Decision | Decide by | Cost if deferred |
|---|---|---|---|
| 1 | What is a tenant? | Day one | Rewrite every query |
| 2 | How does a request resolve to a tenant? | Day one | The most common real breach |
| 3 | Where does isolation live? | First paying customer | Audit everything you wrote |
| 4 | Roles, or permissions? | Customer three | Fix live roles retroactively |
| 5 | Which fields are secret, from whom? | Before your first aggregate endpoint | Find every reporting query |
| 6 | What happens when a workflow fails halfway? | Before money moves | Reconcile by hand |
| 7 | Per-tenant databases? | Wait | (this one is fine to defer) |
(Disclosure: I work on Supero, which generates multi-tenant backends. I have written this so it is useful if you never touch our product; the decisions are the same on any stack. There is one linked section at the end about how we answer them, kept off this page on purpose.)
1. What is a tenant, exactly?
Not rhetorical. Get it wrong and every later decision inherits the error.
"Tenant" usually starts as a synonym for "customer," and then reality arrives: a customer with two departments that must not see each other; a customer who acquires another customer; a reseller managing fifteen accounts; a user who belongs to two tenants and switches.
With one flat tenant_id, the first case forces a fake second account, the third forces an application-level join that bypasses your isolation, and the fourth forces a session hack. All three are things teams actually do, and all three are how cross-tenant leaks ship.
Decide on day one. Pick a hierarchy even if you use one level of it for a year. organisation → workspace → user gives you room for departments and resellers without a migration. Adding a level later means rewriting every query you have.
2. How does a request resolve to a tenant?
The decision most checklists skip, and the one that produces the most real vulnerabilities.
Decision 3 asks where isolation is enforced. This asks where the tenant identity comes from. Subdomain? A signed JWT claim? A session lookup? Or — the one that ships breaches — an org_id in the request body that the caller supplies?
An isolation layer that cannot omit tenant scope is worth nothing if the scope it cannot omit was chosen by the attacker. Same for RLS keyed on a claim your API populates from user input.
Decide on day one, and write it down: the tenant identity comes from exactly one place, it is server-derived, and no request field can override it. Then grep for the places that do it differently, because there will be some.
3. Where does isolation live?
Three honest answers.
In the application. Every query carries WHERE tenant_id = ?. Fast to build, and it fails the moment one developer on one endpoint forgets. Not hypothetical: a statistical certainty as headcount grows. The failure is silent. 200, with too many rows.
In the database. Postgres row-level security, schema-per-tenant, or database-per-tenant. Much stronger, because forgetting is no longer possible: the database refuses. Costs are real: RLS is its own skill and can be subtly wrong; schema-per-tenant makes migrations an operational project; database-per-tenant makes cross-tenant analytics and connection pooling hard at a few hundred tenants. That last option is the physical tier of this same decision. See #7, and do not treat it as a separate choice.
In one shared producer below the application. Be clear-eyed that this is a disciplined version of the first option rather than a third tier: it is middleware. What it changes is that the scope is written once instead of at forty call sites, which removes the per-endpoint mistake, the single most common way isolation actually breaks. What it does not do is make the boundary unforgettable the way the database refusing a query does. Ask any vendor claiming this (us included) two things: does the producer have bypass branches, and what stops a new call site from skipping it entirely?
Decide by your first paying customer. Moving later means auditing every query you have ever written, and you will not find them all.
The test that tells you the truth: ask a developer to write an endpoint that leaks across tenants. If they can — if nothing stops them but their own care — that is your isolation model, and care does not scale.
Apply it to us, since it would be cheap to set a test we pass. We do not pass it cleanly. The shared producer removes the per-endpoint mistake. What it does not remove is three ways around the boundary that need no mistake at all: admin roles have documented exemptions, a user left in the platform's default-tenant gets project-wide read, and a request arriving with no session context returns allow. That last one is unreachable today, because authentication runs first, but it sits in the code. The honest score is "a developer cannot leak by forgetting, and can leak by being provisioned wrong." Better than the first option on this list. Not the same as the second.
The part of that we can demonstrate rather than assert is the narrow part. Re-inline the tenant scope at a call site and the gate that parses our own source fails, exit 1; leave the code alone and all 25 gates pass. That answers "can a developer write the leaking endpoint" and nothing else on the list, which is roughly the shape of evidence to demand from anyone answering this question.
4. Roles, or permissions?
Everyone starts with roles: admin, member, viewer. Everyone ends up needing permissions, because the fourth customer wants someone who can see invoices but not edit users.
The escape is a custom_roles table, and it is the right call. Two things go wrong:
Privilege escalation through role creation. If a tenant admin can create a role, they can create one with permissions they do not have, unless you enforce a ceiling: a created role can never exceed its creator's permissions and can never shadow a builtin. Five lines, missing from a surprising number of production systems.
Roles checked in the UI. If your frontend hides a button and your API does not check, you do not have roles. You have a suggestion.
Decide by customer three. Retrofitting a ceiling after tenant admins have created roles is worse, because you now have live roles violating the rule you are about to introduce.
5. Which fields are secret, and from whom?
Made last, regretted most, and where I have watched careful teams still ship a hole.
Hiding a field from a response is the easy half. A field nobody can see can still be used to compute. If a caller cannot see salary but can write ?order_by=-salary&limit=1, they read the highest-paid employee's name. ?distinct=diagnosis_code returns the value set, and for a low-entropy column the value set is the secret.
Response masking fixes none of it, because the leak is not in the body — it is in which rows came back and in what order.
The fix: the fields a caller may filter, sort, group and aggregate by must be the same set they may see, computed in one place. Two lists maintained separately will drift.
Decide before you build your first aggregate or reporting endpoint. That is where this bug is overwhelmingly introduced: aggregates get written later, by someone else, and take a raw field name.
Our own read-input guard for this is deployed and running in monitor mode, where it logs would deny read and returns the rows anyway. Which is to say the decision is easy to make and slow to finish, on any stack.
The full anatomy of that attack, and what a real fix requires.
6. What happens when a workflow fails halfway?
Your checkout reserves inventory, charges a card, creates an order. The charge succeeds. The insert fails.
Three levels of answer. Nothing: you find out from the customer, which is most early products, and mostly they get away with it. Retries and idempotency keys: necessary and insufficient, because retrying forward does not undo a half-applied sequence, and a non-idempotent money operation applied twice is worse. Compensation: step 4 fails, steps 3, 2 and 1 reverse. That is the saga pattern, and the actual answer.
Three things teams underestimate. Compensation logic is roughly the same volume as the forward path and is the code least likely to be tested, because writing a test that fails step four on purpose is annoying. The reversal can itself fail. Decide now what happens then, because "the reversal didn't reverse" needs an operator, an alert and a runbook. And a compensation engine is not the same as compensation coverage: every individual operation needs a correct inverse declared, and the ones that don't fail silently. Audit them one by one. Ours declares 119 states and 81 transitions across 21 service manifests, 19 forward operations carry a locked inverse, and we have still found mis-wired ones in there.
One precision worth having: for payments specifically, the clean pattern is authorize-then-capture, and you void an uncaptured authorization rather than compensating a completed charge. Compensation is for the steps around the money.
Decide before you take money. Not before you launch — before money moves.
7. Single database, or one per tenant?
The one everybody argues about first and should mostly argue about last. It is the physical half of decision 3, which is why it is here rather than earlier.
For most B2B SaaS, a shared database with strong isolation is correct until you have either a customer contractually demanding physical separation or a measured noisy-neighbour problem. Both are real and both arrive later than you fear. Premature database-per-tenant costs you migrations across N databases, connection pool exhaustion, cross-tenant analytics that were one query and are now a pipeline, and a provisioning flow you now operate.
Decide when a customer asks, or when you measure a problem. The one item where "later" is right.
What this multi-tenant SaaS checklist leaves out
Four more that bite, in rough order of how often I have seen them:
- Per-tenant SSO and SCIM deprovisioning. Arrives with the first enterprise deal and retrofits badly.
- Tenant deletion and data residency. "Delete Acme entirely from a shared database" is a legal obligation, and EU-resident tenancy is a day-one partitioning decision.
- Per-tenant rate limits and quotas. Cheap now, brutal later — this article's own thesis.
- Support impersonation. "View as Acme" is the standard isolation bypass, and it is usually unaudited.
Six of seven above are cheap today. That ratio is the point of the article, and it is why the seventh being deferrable matters: it is the one that looks like the big architectural decision and is not.
Frequently asked questions
What is the best multi-tenant architecture?
Shared database with isolation enforced below the application, until a customer contractually requires physical separation. The isolation layer matters far more than the physical layout.
When should I add multi-tenancy?
Before the second customer organisation logs in. Retrofitting isolation means auditing every query already written.
Is row-level security enough for multi-tenant SaaS?
It is a strong row boundary and the right default on Postgres. It does not cover which columns a caller may see or compute with, and it is opt-in per table.
How do I stop a tenant admin escalating their own permissions?
Enforce a ceiling on role creation: a created role cannot exceed its creator's permissions or shadow a builtin.
Do I need a database per tenant?
Almost certainly not yet. Wait for a contractual requirement or a measured problem.
What to do next
Run decision 2 against your own codebase this afternoon. Find every place the tenant identity is established and confirm none of them reads it from a request field the caller controls. That is a one-hour audit and it is the highest-value item on this list.
Then decision 5: pick a field some role cannot see, and try to sort by it. If the rows come back in the right order, you have found the next fortnight's work.
How Supero answers decisions 3 and 5 is the field-permission piece. What you can take with you if you ever leave is a separate page. Both are kept off this one so this one stays useful. Applied to specific tools: Bubble · Retool · Supabase · Lovable, Bolt and v0. Building these for clients: what to charge.

Top comments (0)