Skip to content

15. Scope — per-user accessibility, per-world focus

An earlier draft of this section read the reference host’s scope handling as a defect against §2. It is not. It is the intended model, and §2 overstated it; this section states what scope actually guarantees so that no one else has to reverse-engineer the answer from a scope predicate.

Accessibility is per user. Focus is per world.

A principal reaches their own data. A world does not fence one part of it off from another part of the same principal’s; a world selects WHICH CAPABILITIES EXIST — the types, realms, producers, views, skills and actions installed in it. That is a real boundary and it is enforced by construction rather than by predicate: a label whose realm is not installed in a world cannot be named in a query there, has no producer to fetch it, and appears in no view.

So two worlds of one principal differ in what they can DO, not in what the principal may see. A canonical spine that both worlds’ realms declare is the same spine, which is the point of a spine.

Between principals, the boundary is absolute. A node is in scope when it carries the acting principal’s id, or names them in its share projection. That is the isolation a shared store depends on, and it is the claim worth testing.

15.2 What this means when you are deciding something

Section titled “15.2 What this means when you are deciding something”
  • Designing a realm: a world is a capability surface. If two worlds should see different DATA of one owner, that difference has to come from what each realm exposes, not from a hope that the world boundary will filter it.
  • Reading a cache: a fetched record is reused for whoever the data it was fetched INTO belongs to, which is the principal for their own data and WIDER for data the deployment declares shared — see §15.4. It is not a leak across a confidentiality boundary, because between worlds of one principal there is no such boundary, and because a declaration that rows are the same for everybody is a declaration that no principal owns them.
  • Making a deployment claim: “isolated per user” is supported. “Worlds are mutually confidential” is not, and is not intended to be.

15.3 What a fetch is keyed by, which is easy to assume wrongly

Section titled “15.3 What a fetch is keyed by, which is easy to assume wrongly”

Within a scope, a fetch is keyed by everything that changes the result set — the producer, the resolved key, the pushed-down filter, any steer or tuning, and the limit and demand in force. A read taken under a cap therefore never serves one taken without. The other direction is allowed, on the conditions §5.13 states: a complete read taken without a cap or filter may serve the same key’s question under one, because the narrower answer is inside it. This is what lets a warm-up in one request populate the cache for another without a truncated answer being served as a complete one.

15.4 Which scope a cached record is kept for

Section titled “15.4 Which scope a cached record is kept for”

The scope a ttl or immutable entry is filed under is the scope the FETCHED data belongs to, not the scope of whoever asked for it first. There are three, and the difference is a cost difference and never a visibility one — no record is ever served to a principal who could not have fetched it themselves:

the label a fetch targets kept for what a second asker pays
the principal’s own data — your realm’s types, and anything the deployment has not declared otherwise that principal the full cold price, per principal
reference data the deployment declares (its own taxonomies and catalogues) the deployment nothing: one entry, whoever asks
shared data the deployment declares (its canonical people, organisations, addresses, documents) the deployment’s organisation nothing within that organisation; the full price in another

So a realm whose producer fills a label of its OWN pays per principal, and should budget for it. A realm joining onto a label the deployment declares reference or shared pays once for the whole deployment — which is also why a realm may not widen a label into either class: declaring visibility: public over a label the deployment already declares shared is refused, and a realm’s own public register stays per principal in the cache, because its rows are its world’s and not the organisation’s.

What stays per principal whatever the label says: nothing is inferred from a producer’s CREDENTIALS. If your producer authenticates as the acting user and you point it at a label the deployment declares shared, you are declaring that its records are that label’s — one row per identity, the same for everybody — and they will be kept that way. Fill a label of your own instead.

15.5 Which positions you must label, and what holds the rest

Section titled “15.5 Which positions you must label, and what holds the rest”

A node is held to you by the labels it names, so a node YOU bind must name a label: a bare MATCH (n) or MATCH (b:Bill)-[:ABOUT]->() is refused, naming the node as you wrote it and offering both halves it needs — a name and a label. That node is part of your answer: it decides which rows exist and how many, and nothing can be said about which node it is.

A position only a predicate reaches is yours to leave unlabelled. The far end of a pattern predicate, and the far end of a hop inside an EXISTS, NOT EXISTS, COUNT or COLLECT subquery, produce no row and no column of your query. Each is held to you there by its own condition — the labels the STORED node carries decide it: a node carrying a declared non-private label is reference or shared data and answers; any other node must be yours. A relationship from your own node proves nothing about the node at its other end, so this is a condition on the node itself, never on how it is reached.

A subquery answers the same question in either spelling. EXISTS { (s)-[:SUPPLIES]->() } and EXISTS { MATCH (s)-[:SUPPLIES]->() } are one construct written two ways, and so are the COUNT forms; a position they both reach is held by the same condition, and neither spelling is refused where the other answers. The same holds for a far end you name without a label ((x)), for an undirected or variable-length hop, and for a subquery nested in another. A COLLECT body has only the one spelling, and is held the same way.

Two limits stay, and they are the same in both spellings: the subquery’s pattern must start from a node bound OUTSIDE it, and a position no relationship reaches (EXISTS { MATCH (s)-[:R]->(:T) MATCH (y) }) must name a label. Each of those is a scan of the whole graph rather than a constraint, and holding one position states no scope over a scan.