Blog post

Tenant Is Trust, Not Data

Why tenant scope cannot be treated as a field that flows through the system, and what it means to establish it as a trust boundary instead.

I used to think tenant ID was just another field moving through the app.

It arrived in the request. It went into queries. It scoped the data. It worked. I moved on.

That model is wrong in a way that only becomes visible once you think carefully about what "works" actually means in a multi-tenant system.

How it looked at first

The earliest version of my mental model was close to this:

user makes a request → request includes tenant context → backend uses it to filter queries → right data comes back

Clean. Reasonable. And in a demo, completely fine.

The problem started when I asked a different question: where does that tenant context come from, and who decides it?

If the answer is "from the request," the next question is: which part of the request? A URL parameter? A form field? A header? The request body?

And then: can that value be wrong? Can it be manipulated? What happens if it is?

That is the moment when the model breaks.

The category distinction that changed everything

Data moves freely. It flows through the system, gets transformed, gets returned to the UI, gets modified. Treating it loosely is usually fine.

Trust boundaries are different. They define what a user is allowed to access. They are not supposed to be negotiable for each request. They are established once, from a trusted source, and held immutable for the duration of the operation.

Tenant scope is a trust boundary.

It says: this user, in this request, can only act within this tenant. That is not a preference or a default that can be overridden. It is a constraint derived from authentication.

Once I put tenant scope in that category, the design questions became much sharper.

What goes wrong when tenant is treated as data

The failure modes are predictable once you see the pattern.

Tenant from the URL (/api/tenants/123/users) means a user who changes the number in the URL might access a different tenant's data. The backend trusts the value because it arrived in a well-structured request.

Tenant from a form field or query parameter means a developer can copy a form and forget to update the tenant ID. Or a user can inspect the request and try a different value.

Tenant from the request body means frontend code controls an identity boundary that should be controlled by the backend.

In each of these cases, the system appears to work. Happy path requests succeed. Standard flows complete correctly. The isolation looks fine.

But the boundary is not actually enforced by the system. It is enforced by convention: by the assumption that nobody will send the wrong tenant ID. That is not a trust boundary. That is a polite suggestion.

How to actually establish tenant scope

The only correct source for tenant scope is authentication.

The JWT or session credential that proves who the user is should also prove which tenant they belong to. The backend extracts that tenant from the credential during authentication, not from the incoming request parameters.

Once it is established, it goes into request context as an immutable value. Not passed through function parameters. Not re-read from the request later. Not re-computed at the service layer. Set once, used everywhere, not changeable for the duration of that request.

The frontend may express an intended workspace when a user belongs to multiple organizations. That expression is a signal, not authority. The backend decides whether the user is allowed to act in that workspace. The frontend never grants scope.

There is no partial version of this

This is one of the architectural constraints where partial compliance does not help.

If tenant scope can be established from the request in one endpoint, the trust boundary is already weak. The question is no longer "is the system correctly isolated?" but "how far can the wrong tenant ID travel before something stops it?"

That is not a comfortable question for a production multi-tenant system.

Either the tenant is derived from authentication and held immutable, or it is not. Either every operation is scoped by a trusted tenant value, or some operations are not.

The invariant has to be complete to be meaningful.

What this changes in practice

In practice, this meant rethinking where tenant scope lives in the stack.

Not in URL routing. Not in service function parameters. In request context, established from the JWT, immutable for the life of the request. Every database query filtered by it. Every authorization check including it. No exceptions for "internal" operations or "trusted" paths.

The result is a system where the question "can User A access Tenant B's data?" has a structural answer, not a convention-based one.

That is a meaningfully different kind of guarantee.

Continue exploring

Follow the same line of thought through themes, tags, or a broader local search across the archive.

Keep following the thread.