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 rowsSeparate stores
Isolation strengthEnforced below the appSurvives app compromise
Cross-entity reportingStraightforwardNeeds a deliberate aggregate layer
Operational costOne estate to runGrows per entity
Typical fitMost multi-entity groupsRegulated 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.

Questions

Short answers,
in full.

The questions this article gets asked most, answered so each one stands on its own.

Talk to us
What is multi-entity data isolation?

Keeping each legal entity's data separated from the others in a way that holds regardless of what application code does. The common implementation is a shared table with an entity_id column and a filter applied in code, which is a convention rather than a boundary. Enforcement in the database — row-level policies evaluated on every query — makes isolation something developers cannot violate rather than something they must remember.

Why isn't filtering by entity_id enough?

Because it assumes every query, in every endpoint, written by every developer, forever, remembers the filter. That fails in entirely ordinary ways: a new report, an admin tool, a background job, a migration script. Each omission is one step from one client's data appearing in another's screen. If isolation depends on someone remembering, it is a convention, not a boundary.

How can a group report across entities if the data is genuinely isolated?

Group reporting is served from governed aggregates rather than by granting cross-entity access to raw records. Aggregates are computed under a role permitted to read across the estate, materialised, and exposed to directors within their own access scope. The aggregate becomes the shared object; nobody's underlying records become visible to a peer entity.

When do you need separate data stores rather than row-level policies?

When the obligation is to survive a compromised application, which is common for healthcare groups and for contractual or regulatory separation. It is a real trade: separate stores give stronger isolation but require a deliberate aggregate layer for cross-entity reporting and cost more to operate per entity. Row-level policy enforcement is the right default for most multi-entity groups.