can is the only gate shape in Alfiz. Every route handler, server action, and API endpoint that enforces access control calls can (or its throwing form require) on a concrete permission key at a concrete scope. The key is verified against your catalog before evaluation, so a key the catalog does not declare raises UnknownPermissionError, which is a programming error, not a denial, and must never be mapped to a 403.
can(principal, key, scope?)
PrincipalRef
required
Identifies who is being checked. Either
{ userId: string } for a human
user or { serviceId: string } for a machine principal. The client
resolves the full subject closure (groups, organizations, implicit
directs: and orgof: groups, and everyone) from the provider and
caches it for the duration of subjectCacheTtlMs.K | readonly K[]
required
A permission key (or array of keys) declared in your catalog. If you pass
an array, the check returns
true when the principal holds any of the
listed keys at the given scope. The key is a typed string inferred from
your catalog, so the TypeScript compiler rejects keys that are not in the
catalog at literal call sites, and the runtime rejects them everywhere
else.LooseScopeId<S>
Optional. A scope instance id of the form
<scopeType>:<instanceId>, for
example docs.doc:abc123 or docs.folder:project-x. When a scope is
provided, the client resolves its full ancestor chain and checks grants at
the scope itself, at every ancestor, and at *.Scope ids hint rather than gate: a literal autocompletes every declared
<scopeType>: prefix, while an id built from a variable flows through
unchanged, because the instance half of a scope id is runtime data.CheckOptions
Optional.
{ observe } controls whether this check emits a metrics
observation. Defaults to true, and is inert unless the client was
constructed with metrics. View-as previews pass false, so attribution
never follows the preview.Return value
Promise<boolean>
Resolves to
true when the principal holds the key at the given scope
(directly or through a covering grant higher in the hierarchy), and false
in every other case including inactive principals, expired grants, and
suppressed grants. This method never throws for a denial, only
require throws.can.fresh(principal, key, scope?)
can.fresh has the same signature as can but bypasses both the subject closure cache and the object ancestor-chain cache, fetching fresh data from the provider on every call.
can.fresh in place of can for:
- Destructive actions (
delete,purge,destroy_*) where acting on a stale “yes” is irreversible. - Just-in-time elevations. Time-limited grants where the expiry boundary matters at the second level, not the minute level.
- Any endpoint where a user has just had access revoked and you need to see that revocation immediately rather than waiting for the TTL to expire.
require(principal, key, scope?)
can. Identical semantics: it uses the cached path (not fresh), verifies the key against the catalog, and then throws AccessDeniedError if the principal does not hold the permission. Use this in server actions and route handlers where a thrown error is the right control-flow signal.
AccessDeniedError carries a typed reason field:
Cache semantics
Alfiz caches two things independently, both parameterized by provider invalidation events:- Subject closure. The principal’s group memberships, org memberships, and implicit groups. Cached for
subjectCacheTtlMs(default 30 s) and bust immediately when the provider emits auser,subject,role, orallinvalidation event. - Object ancestor chain. The ancestry path from a scope instance up to
*. Cached forobjectCacheTtlMs(default 60 s) and bust immediately when the provider emits ascopeevent for any scope in the chain.
can call re-evaluates the decision from freshly obtained (or cache-hit) closures.
can.fresh bypasses both caches and always fetches from the provider, regardless of what is cached.