Tenant isolation is a schema decision, not a middleware one
The failure mode everyone ships first
The intuitive way to build multi-tenancy is a middleware that reads the tenant from the request and stashes it in a context. Every controller then remembers to filter by that tenant. It works — until one query forgets.
That one forgotten where tenant_id = ? is not a bug you catch in code review.
It's a data breach you find in a support ticket.
Make isolation structural
The fix is to stop relying on developers remembering. Scope isolation to the layer that touches every query:
- A global query scope that injects the tenant predicate into every read.
- A model event that stamps the tenant on every write.
- Foreign keys that make a cross-tenant reference impossible to persist.
Now the safe path is the default path. A developer has to go out of their way to leak data, rather than out of their way to prevent it.
What it costs
You pay for this with a little ceremony: system jobs and admin tooling need an explicit, audited way to run without the scope. That escape hatch should be loud, centralized, and logged — not a boolean flag sprinkled across the codebase.
I'd rather have one well-guarded door than a hundred windows I have to remember to lock.