Architecture · 8 min read · Aug 12, 2026
A filter is not a boundary.
Every multi-entity system claims data isolation. Most implement it as a WHERE clause in application code. The difference only becomes visible the day someone ships a query that forgets it — and by then it is an incident, not a design discussion.
What most systems actually do
The common pattern is a shared table with an entity_id column, and application code that appends a filter on every query. It works, in the sense that correct code returns correct results.
The problem is what it assumes: that every query, in every endpoint, written by every developer, forever, remembers the filter. That assumption fails in ordinary ways — a new report, an admin tool, a background job, a migration script, a debugging endpoint someone left in. None of those are exotic. Each is one omission away from one client's data appearing in another's screen.
The test is simple. If a developer writing a new query has to remember something in order for isolation to hold, you do not have isolation. You have a convention.
Enforcement at the boundary
The alternative is to make the database refuse. Row-level security policies attach the rule to the table itself, evaluated on every query regardless of which code path issued it. A request that asks for rows it is not entitled to gets nothing back — not because the application filtered them, but because the data layer would not return them.
That inversion matters more than it sounds. It changes isolation from something every future developer must uphold into something they cannot violate. New endpoints inherit it. Background jobs inherit it. The debugging tool someone adds under deadline inherits it too.
Ontilus has taken this position under commercial pressure. On one build, an outside developer proposed replacing the database-enforced model with a hand-rolled application layer. We rejected it and kept enforcement at the boundary, because the authorisation model was the part that had been attack-tested and the proposed replacement moved the rule back into code that has to remember.
Hard separation, where it's warranted
Row-level policies are the right default. Some obligations go further — healthcare groups and regulated entities frequently need separation that survives a compromised application entirely, which means distinct data stores rather than distinct rows.
That decision is a trade, and it should be made deliberately rather than assumed in either direction:
| Policy-enforced rows | Separate stores | |
|---|---|---|
| Isolation strength | Enforced below the app | Survives app compromise |
| Cross-entity reporting | Straightforward | Needs a deliberate aggregate layer |
| Operational cost | One estate to run | Grows per entity |
| Typical fit | Most multi-entity groups | Regulated or contractual separation |
How group reporting survives isolation
The objection to real isolation is always the same: the board needs to see across entities. Both can be true, and the resolution is that group reporting is served from governed aggregates rather than by granting one entity access to another's records.
Concretely: aggregates are computed under a role that is permitted to read across the estate, materialised, and then exposed to directors through their own access scope. A director sees group totals. Nobody's raw records become visible to a peer entity in order to make that happen. The aggregate is the shared object, not the underlying rows.
Auditability is part of the boundary
Isolation without a record of access answers only half the question an auditor asks. The other half is who read what, and when.
That means logging access events, not only mutations. Most systems log writes because writes change things; reads are the ones an auditor asks about after an incident. Both belong in the trail, along with full lineage from a figure in a report back to the records it came from.
Questions worth asking a vendor
- Is isolation enforced in the database or applied in application code?
- If a developer writes a new query tomorrow and forgets the entity filter, what happens?
- How is group-level reporting produced without granting cross-entity read access to raw records?
- Are read events logged, or only writes?
- What is the data residency, and can it be pinned to a region we name?
The answers separate systems that were designed for multiple entities from systems that had a column added.
Dealing with this in your own group?
We answer scoping questions before there's a contract in sight — including the ones about cost and data handling.