What can a relation-tuple store's expand call answer that its check call cannot, and when do you need it?
answer
- one boolean versus a structure
- expand takes no subject
- check stops at the first grant
- its leaves may still be usersets
- an admin surface, not a hot path
basics
~20 sCheck answers one boolean for a subject, object and relation, and says nothing about why. Expand takes no subject and returns the rule tree — unions, rewrite hops, userset leaves — so a human can justify a grant.
solid answer
~50 sThe two calls answer different questions. **Check** takes an object, a relation and a subject and returns a boolean; it is the call on the request path, it may short-circuit as soon as one arm of a union grants, and it deliberately tells you nothing about the route it took. **Expand** takes an object and a relation only, and returns the tree the schema produces for it: the union arms, the rewrite hops, and leaves that are either concrete subjects or usersets such as `org:northfield#member`. You need expand whenever a human has to see or justify a grant — a `who may sign off this component` panel, an explanation of a denial, or a revocation review that must find every arm granting authority before one is removed. Note that expand does not hand you a flat user list; the userset leaves still have to be walked, and you choose how far.
code
json · 13 lines{
"object": "airframe:G-ABCD",
"relation": "inspector",
"tree": {
"union": [
{ "leaf": { "subjects": ["user:15", "user:22"] } },
{ "hop": {
"followed": "airframe:G-ABCD#holder@org:northfield",
"child": { "leaf": { "subjects": ["org:northfield#member"] } }
} }
]
}
}go deeper
Remember the inputs: check takes a subject and answers yes or no, expand takes only an object and a relation. That difference explains everything else about the two calls.
Explain the short-circuit. A check stops at the first granting arm, which is why it is cheap and why it cannot tell you the route it took; expand returns the structure instead.
Show where each one lives in a running system: check behind the request handler, expand behind an administrative surface with its own authorization, and neither substituted for the other under load.
The judgment is what your product owes a user who was denied. If authority must be explainable and reviewable before a revocation, the expand surface is a first-class feature to be designed and guarded, not a debugging tool.
## Two calls, two shapes A relation-tuple store exposes a small number of call shapes, and the two that matter for a single object are **check** and **expand**. They are not two sizes of the same call; they answer different questions and are used in different places in a system. Check is the one your endpoint makes. It takes `(object, relation, subject)` and returns a boolean: *may engineer 15 sign off the hydraulic pump?* Expand is the one your admin surface and your operators make. It takes `(object, relation)` and returns the structure of ways that relation can be satisfied on that object. ## What check gives you, and what it withholds | property | check | expand | |---|---|---| | input | object, relation, subject | object, relation | | output | one boolean | a tree of rules with subject and userset leaves | | may short-circuit | yes, as soon as one arm grants | no, the structure is the answer | | used on the request path | yes | rarely — it is an operator and admin call | | answers `why` | no | yes, by showing the route | The short-circuit is the reason check is cheap and also the reason it cannot explain itself. As soon as one arm of a union grants, the traversal stops; the other arms are never explored, so there is no complete set of reasons to return even if the API wanted to give you one. Check is also the only one of the two whose answer is a decision. The store decides; your endpoint enforces. A false from check is not an HTTP status — the endpoint chooses what a denial looks like to the caller. ## What expand returns Expand returns the rule structure, evaluated against the tuples that exist: - **union and intersection nodes** mirroring the schema's rewrites; - **hop nodes** for tuple-to-userset rewrites, naming the tuple that was followed; - **leaves** holding either concrete subjects (`user:15`) or usersets (`org:northfield#member`). That last point is the one people miss. A userset leaf is still a set. Expand hands you the shape, not a resolved roster, and turning `org:northfield#member` into four hundred names is a second traversal you choose to run — or choose not to, and render as *members of Northfield* instead. ## When you actually need it 1. **Showing authority on a record.** A maintenance record that displays *who may sign this off* needs the structure, not four hundred repeated checks. 2. **Explaining a denial.** An engineer who expected to sign off and could not needs to see which arm was supposed to grant and which stored fact is missing — usually the custody tuple, or a fitting that was never recorded. 3. **Reviewing before a revocation.** Before removing an engineer's direct `inspector` tuple, expand tells you whether they also reach the relation through the holding organisation, which is the difference between a revocation and a no-op. 4. **Auditing the schema against reality.** Expand over a sample of objects shows whether a rewrite grants more widely than the rule you meant to write; it is the cheapest review tool you have for a schema change. ## What expand is not It is not an optimisation of check. It is strictly more work: no short-circuit, a whole structure returned, and potentially a large one. Never put it on a hot request path in place of a check because it looks more informative. It is also not the reverse question. *Which of these many objects may this principal act on* is a third call shape with its own cost profile and its own design problems around ordering and paging, and it is a separate subject from the two single-object calls here. ## A practical convention Keep the two calls in different layers of your service. Check belongs behind the authorization decision your request handler makes, with an unremarkable boolean return. Expand belongs in an administrative or support surface, behind its own authorization — because the shape of who may act on an object is itself sensitive, and an expand endpoint exposed to ordinary callers tells them about relationships and organisations they may otherwise never see.
- Why can a check not return the reason it allowed a request?Because it stops at the first arm that grants. The traversal is deliberately lazy, so the other routes are never explored and the store has no complete reason set to return. If you need the route, you are asking a different question and should use expand against the same object and relation.
- Should an expand endpoint be exposed to ordinary users?No. The tree names organisations, usersets and relationships a caller may have no business seeing, so it needs its own authorization and usually belongs to a support or administrative surface. Rendering a userset leaf as a group label rather than a roster is also a sensible default.
saying these in an interview costs you the question
- Thinks expand is a faster or batched form of check
- Expects expand to return a flat list of individual users
- Puts an expand call on the hot request path for a single decision
- Assumes a check can report which rule granted access
- Exposes the expand result to any caller without its own check