Skip to content

Notes · 24 July 2026 · 5 min

Multi-tenant isolation is not a WHERE clause

Filtering by tenant id and isolating tenants are different things, and the difference only shows up on the day it matters most.

Most multi-tenant systems begin the same way. There is a tenant id on every table, every query carries `WHERE tenant_id = ?`, and it works. It goes on working for a long time, which is the problem — the design is not tested by normal operation, only by the day somebody forgets.

We built a vertical SaaS platform serving Indian logistics companies: lorry receipts, trip profitability, GST invoicing, freight bidding. Several transporters, one platform, and a category of data — what a competitor charges, which routes make money — where a leak is not an inconvenience but the end of the product.

The failure mode is one missing clause

With filtering, correctness depends on every query, in every code path, written by every developer, forever. There is no structural guarantee. A new report, a background job, an admin screen, a hurried fix on a Friday — any one of them can omit the clause, and nothing fails loudly. The page simply shows more rows than it should, to somebody who has no idea they are seeing another company's freight rates.

That is the whole argument. Not that filtering is wrong, but that it puts a security property in the least reliable place available: human discipline, repeated indefinitely.

What we did instead

Data is isolated per tenant, sitting on a shared identity and billing core. The shared part is deliberately small — who you are, what you pay for, which tenants you can reach. Everything a transporter would consider theirs is separated rather than co-mingled and filtered.

The practical consequence is that forgetting is no longer catastrophic. A query written without thinking about tenancy returns nothing or fails, rather than quietly returning too much. You have moved the guarantee from convention into structure, which is the only place it survives staff turnover.

Roles are defined by each tenant for itself rather than imposed by the platform. A four-person operator and a two-hundred-lorry fleet do not have the same idea of what a dispatcher may do, and a fixed role table forces one of them to work around it — usually by sharing a login, which undoes the access control entirely.

The cost, honestly

Isolation is more work to build and more work to operate. Migrations run per tenant. Cross-tenant reporting, which is trivial with a filter, becomes a deliberate feature you have to design. Onboarding a tenant is provisioning, not an insert.

It is not automatically the right answer. If tenants are individual users of a consumer product, filtering is proportionate and isolation is overengineering. The question that decides it is not how many tenants you have — it is what a leak would cost. If the answer is a support ticket, filter. If the answer is that a customer would leave and tell their industry why, isolate.

Ship the platform, not the fork

The last piece is that the suites are containerised, so a new vertical ships without forking the platform. Freight was the first one live.

Forking is the standard way vertical SaaS dies. The second industry gets a copy of the codebase with its own quirks, then the third, and within two years there are three products wearing one name and every fix has to be applied three times. Keeping the platform single and the vertical a container is more discipline up front and the only version that survives a third customer segment.

This came out of Pravah — FreightOS. The case study says what it does and what it runs on.

Got something that needs building?

Tell us what it has to do and who has to use it. You get a written scope, a fixed quote, and an honest note about anything we think you should not build.

Start a conversation