Data isolation
Isolation Is Tested, Not Configured
A configuration flag named after a behaviour is a claim about that behaviour, not the behaviour itself. When isolation is a filter rather than a wall, the only evidence worth having is a write one tenant cannot see.
What a tenant actually is here
The hosted studio has no user table. It has no accounts, no tenant records and no per-customer configuration. Its entire notion of who is asking is one function that hashes a session token into a workspace identifier, and a scope that runs the request inside whatever workspace that produced.
That is a smaller mechanism than it sounds, and the smallness is the point: a workspace is its token. There is no registry to fall out of step with reality, because there is no registry. Two requests carrying the same token are the same tenant, and two carrying different tokens are different tenants, with nothing in between to get wrong.
It also means the boundary is only as good as the discipline that every request passes through that scope. There is no filesystem separation and no second database doing the work as a backstop.
The flag that is not the switch
There is an environment variable on that deployment called ISOLATE_BY_SESSION. It is set to false. Per-visitor isolation works anyway.
The variable gates a different path (the one that derives a workspace from an API key) and has no bearing on the session path that public visitors actually use. Anyone auditing this deployment by reading its configuration would conclude that tenants share a graph. They do not. Anyone who had switched it on expecting it to be the thing protecting visitors would have been equally wrong, in the more dangerous direction: relying on a control that was not the control.
A flag named after a property is not the property. It is a claim about the property, made by whoever named it, and claims decay as code moves underneath them.
Isolation by filtering
It is worth being precise about what kind of boundary this is, because the failure modes differ sharply.
The entities are not partitioned into separate stores, or separate databases, or even separate named graphs. They all live in one graph, tagged with the workspace they belong to, and separated at read time by a scope the request carries. Isolation here is a filter that every query path must apply, not a wall that exists whether or not anyone remembers it.
That distinction decides what a bug looks like. A partitioned system fails loudly: the query points at the wrong store and returns nothing, or errors. A filtered system fails quietly: a query path that forgets the filter returns more rows than it should, and more rows looks like a working feature. Nothing throws. The extra rows are somebody else's model.
A worked check
Because the boundary is a filter, reading the code is not sufficient to believe in it. The only evidence worth having is a write in one tenant that a second tenant cannot see.
Two sessions were opened against the live server. An agent run was made under the first, which added entities and left proposals outstanding. The second was left alone. Then both were read, along with a request carrying no session at all.
| Session | Entities | Outstanding proposals |
|---|---|---|
| A: the agent ran here | 26 | 3 |
| B: untouched | 23 | 0 |
| no session header | 593 | not measured |
The write is in A and not in B, which is the claim, demonstrated rather than asserted. It took one round trip and it is the only artefact in this article that would survive a refactor.
The third row is the interesting one
A request that carries no session token at all does not fail. It resolves to the shared workspace (the default one, belonging to nobody), which on a long-lived public server accumulates everything anyone ever wrote without a token. That is what the 593 is.
Two consequences follow. A client that forgets to send its token does not get an error telling it so; it gets a large, plausible, shared model, and it may well write into it. And a visitor whose first request is a page load rather than an API call cannot set a header at all, because browsers do not put custom headers on navigations, so something in front has to attach one, or the visitor lands in the commons by default.
Defaulting to a shared space is a defensible choice for a public demonstration and an indefensible one for a paying tenant. What makes it dangerous is that the two are the same code path, distinguished only by whether a header happened to be present.
The part that surprises people
This did not always work. An earlier build shared the model across sessions while keeping typed requirements private, and that behaviour was documented (in our own notes, and in material we shipped to customers) as a caution to assume every write was public.
The code was fixed. The documentation was not, and it outlived the defect by long enough that we were still warning people about a problem that no longer existed, in a document they had paid for. A stale warning is not harmless: it teaches users to work around a limitation that is gone, and it costs you the credibility you need when the next warning is real.
Both directions of stale belief are expensive. A flag that claims a property it does not provide, and a document that claims a defect that no longer exists, fail the same way: somebody makes a decision on a description of the system rather than on the system.
What to take from it
- Prove isolation with a write, not a reading of the code. Create in one tenant, fetch from another, delete. One round trip, and it is the only evidence that is about your deployment rather than about someone's intentions.
- Know whether your boundary is a wall or a filter. A filter that is forgotten returns more data and looks like it worked. Walls fail loudly; filters fail silently and in the direction of disclosure.
- Treat a missing credential as an error, not a default. Resolving an absent token to a shared space means the failure mode of a broken client is a data leak rather than a 401.
- Date your warnings. A documented defect needs re-checking on the same schedule as the code that caused it, or you will be the last person still believing your own release notes.
U.S. Provisional Patent App. No. 64/073,689. Patent Pending.