Skip to main content
explain is the auditability surface of Alfiz. It runs the same evaluation as can, producing an identical boolean outcome, but returns the full working: which grant rows matched, which revoke rows suppressed them, what the computed object closure was, and whether the result was reached through ancestor implication rather than a direct grant match. Use explain when debugging unexpected access decisions, building audit UIs, or powering “why can you see this?” transparency features in your product.

client.explain(principal, key, scope?)

PrincipalRef
required
Identifies who is being explained. Either { userId: string } for a human user or { serviceId: string } for a machine principal.
LooseKey<K>
required
A concrete permission key declared in your catalog. Unlike canAny, you cannot pass a wildcard pattern here. explain always explains a specific leaf. The key is verified against the catalog before evaluation; an unknown key raises UnknownPermissionError.
ScopeId
Optional. The scope instance to explain the decision at, for example docs.doc:abc123. Omit for a global-scope explanation. When provided, the object closure (scope → ancestors → *) is included in the return value.

Return value

boolean
true when the principal currently holds the key at the given scope. identical to what client.can(principal, key, scope) would return.
GrantRow[]
The unexpired grant rows that would allow this key at this scope. Each GrantRow carries its id, subject, scope, pattern or roleId, optional expiresAt, and the provenance record describing who created the grant and how. Empty when allowed is false and no suppression is involved.
RevokeRow[]
Personal revoke rows that suppress the access. When matchedRevokes is non-empty, allowed is always false, because revokes always win, regardless of how many matching grants exist. Each RevokeRow carries its id, userId, pattern, scope, and provenance.
ScopeId[]
The resolved ancestor chain of the requested scope: [scope, ...ancestors, "*"]. For a global-scope check this is ["*"]. This is the set against which grants and revokes were matched. It tells you exactly which ancestor scopes were in scope for the evaluation.
boolean
Whether the principal was active at evaluation time. An inactive principal always evaluates to no access (allowed: false) even if matchedGrants is non-empty.
boolean
true when access was granted through §7.5 ancestor implication rather than a direct grant match. This happens when a permission leaf is declared with impliedOnAncestors: true and the principal holds a grant of that leaf at a descendant scope of the requested scope. When implied is true, matchedGrants will be empty (the direct match found nothing) but allowed is true. The grants responsible are reported in impliedBy.
GrantRow[]
The grants at a descendant scope that produced an implied allow. Empty unless implied is true. matchedGrants keeps its exact meaning: rows matching at the requested scope, so this is where “which grant, and at which scope, implied this?” is answered.

When to use explain

explain is a diagnostic and auditability tool, not a gate. Do not replace can with explain on hot paths, because explain does the same evaluation work and returns more data, which means it allocates more objects and is more expensive to call at high frequency. Use explain for:
  • Debugging during development. Unexpected true or false from can in tests.
  • Audit log UIs. Displaying “why does this user have access to this document?” alongside the grant history.
  • “Why can you see this?” transparency features. Showing users which of their roles granted them access.
  • Access review tooling. Surface the grant provenance chain to reviewers.
  • Automated tests. Asserting that a specific grant row (by id or pattern) is the one conferring a permission.
explain draws from the same caches as can. If you need a fresh explanation that bypasses the cache, take a snapshot with { fresh: true } and call snap.explain(key, scope) instead.

Examples

Basic explanation

Reading the explanation result

Building an audit UI component

Using snapshot.explain for synchronous access in renders