can() call.
The grant row
A grant row is the fundamental unit of access. Its TypeScript shape is:roleId or pattern is set on any given row. A grant either names a role (and inherits all that role’s patterns) or carries a raw permission pattern directly. The scope defaults to * (the global scope) when omitted. The provenance field records who or what created the row; it is required on every write.
A grant with
scope: "*" satisfies every scoped check, because the global scope is in every object closure. Granting someone docs.files.read globally means they can read every document, regardless of which folder it lives in.Provenance
Every grant and every revoke carries aprovenance field. It is a required part of every write, validated before any row is stored:
Creating a grant
Useapp.createGrant(input) to write a single grant row. The full input shape is:
app.createGrants(inputs, provenance). It validates every input before writing any row, emits one audit entry for the batch, and fires one invalidation event per distinct subject rather than one per row.
Revoking access
A revoke is a personal exclusion. Only individual users may hold revokes; groups and service principals do not. The revoke row shape is:Negative always wins, scope-inclusively
A revoke at any scope suppresses matching access at that scope and every descendant scope, regardless of where the positive grant sits. This rule is fixed and not configurable. The consequences are deliberate:- A revoke at
docs.folder:9suppresses grants ondocs.doc:123(a child of folder 9), even if the grant was made directly on that document. - A global revoke (
scope: "*") suppresses matching access everywhere. - A scoped revoke does not suppress access in other subtrees. It is surgical rather than a global erasure.
Effective access formula
Alfiz evaluates a check by combining three things:- The subject closure. The user, every group they belong to (and those groups’ ancestors), their organizations, and
everyone. - The object closure. The target scope, every ancestor scope, and
*. - The revoke set. The user’s personal revokes whose pattern matches and whose scope appears anywhere in the object closure.
checkKey function that makes this decision is:
true only when matchedRevokes is empty and matchedGrants is non-empty.
Expiry
Grants accept an optionalexpiresAt (epoch milliseconds). An expired grant stops matching checks exactly as a deleted one would, but the row remains in storage for audit purposes, so you can always answer “who had access to this document last Tuesday.”
Listing a user’s effective permissions
Callalfiz.heldKeys(principal) to get every permission key the principal holds at any scope. This is the union of all grant-matched keys across the subject closure, filtered by global-scope revokes:
heldKeys answers “what can this person do anywhere?” It uses keyHeldAnywhere semantics: a scoped revoke narrows one subtree but does not erase a key held elsewhere. This is the right question for conditional UI, meaning “should this button exist at all”, when the concrete scope is not yet known.alfiz.explain(principal, key, scope?). It returns a CheckExplanation with the matched grants, matched revokes, and the final allowed verdict: