Skip to main content
Access requests give users a way to ask for permissions they don’t currently hold. Rather than bypassing your authorization model, a request is a proposed grant tuple, (requester, role-or-pattern, scope, expiry?), that sits in a pending workflow until every approval stage clears. When the final stage approves, that approval is the act of writing the grant row. Denial writes nothing. The request system adds no new access semantics; it is a workflow that gates row creation.

What a request is

Every access request is an AccessRequest object with a proposed grant payload, a justification record, a snapshot of the approval stages, and a workflow state:
The stages snapshot is intentional: because policies reference layers ("2 layers up") and role ids rather than named individuals, a pending request survives org restructuring and org-root promotion without modification. The actual approver identity resolves fresh at evaluation time.

Declaring requestability in the catalog

Nothing is requestable by default. You opt in at the catalog level, either on a role or on a scope type. A role can carry a requestable block; a scope type declares requestable inside ScopeTypeInput.
Global access (scope *) is always requested through requestable roles, not patterns. Pattern requests require a scope whose type declares requestable, because the requestability declaration lives on the scope type, not the permission itself.

Submitting a request

Users submit requests through app.submitRequest. Supply a requesterUserId, exactly one of roleId or pattern, an optional scope, a justification map (prompt id → answer), and an optional proposedExpiresAt for time-bound access.
For a pattern request scoped to a resource instance:
If the requestability declaration sets maxDurationMs, Alfiz enforces the cap automatically. Omitting proposedExpiresAt when a cap is set does not bypass it. The cap becomes the expiry.
At submission, Alfiz runs any leading auto-approval stages immediately. If every stage passes at once, the request resolves to approved and the grant row is written before submitRequest returns.

Request workflow states

The request has been submitted and is waiting for a human decision on the current stage. The stageIndex field indicates which stage is active.
Every stage passed. If all stages were auto predicates and every predicate evaluated to true, the request resolves without any human decision. The grant row is written atomically with the state transition.
A human approver advanced the final stage. The grant row is written as part of that decision. Approval is row creation, with full provenance linking the row back to the request.
Any approver at any stage denied the request. No grant row is written. Denial at any stage is final.

Time-bound requests and just-in-time access

A request may carry a proposedExpiresAt timestamp. When approved, the resulting grant row carries that expiry. This pairs naturally with can.fresh on destructive or sensitive surfaces, since can.fresh bypasses the subject-side cache and evaluates the grant’s expiry at the moment of the check, so temporary elevation takes effect and expires precisely.
The requestability declaration can enforce time bounds from the catalog side:
  • requireExpiry: true means every request to this role or scope type must propose an expiry; Alfiz rejects open-ended requests.
  • maxDurationMs caps how far into the future proposedExpiresAt may be, even if requireExpiry is not set.

Where requests live

Requests are homed where the resulting grant row would live:
  • Global-scope requests (role requests with no scope, or with scope *) run on the org root Application. If your Application is not the org root, submitting a global-scope request is rejected.
  • Instance-scoped requests (pattern requests targeting scopeType:id) run on the Application that owns that scope.
Under federation, the AccessRequest shape is identical against every provider, so workflows deepen without migration.

Approver operations

1

List your queue

Call listApproverQueue(approverUserId) to fetch every pending request the given user may currently decide, based on their role grants and position in the reporting hierarchy.There is a third source of eligibility: an administrative override. A holder of alfiz_internal.requests.decide_request at the global scope may decide any stage, and those requests appear in their queue too. It is the escape hatch for a stage nobody can fill, such as a named_approvers stage whose role has no holders, or a management stage for a user with no manager.
2

Decide a request

Call decideRequest with the request id, a deciderUserId, and a decision of "approved" or "denied". Optionally attach a note.
Alfiz validates that deciderUserId is entitled to decide the current stage before applying the decision. Approving the final stage triggers the grant write atomically.
3

Cancel a request

The requester may cancel a pending request at any time:
Cancelled requests do not appear in approver queues. Alfiz cancels pending requests automatically on the two deletion paths, each narrowly: deleteSubject on a user: subject cancels the requests that user filed, and deleteScope cancels the pending requests targeting that scope. Deleting a group: or service: subject cancels nothing, because neither files requests.

Errors on the request path

Every request write rejects through ProviderWriteRejectedError, whose code tells you how to respond:
Approver entitlement is resolved at decision time rather than when the request was filed. The manager chain and role membership are read fresh. A reorg between submission and decision routes the request correctly.
@alfiz/core also exports RequestStateError, thrown by the pure applyDecision helper when a decision is applied to a non-pending request. You only see it if you drive the state machine yourself; the Application converts the same condition into the conflict rejection above.

Building the request surfaces

Alfiz ships no request UI. What it ships is everything the UI needs, on the Application, so a request form and an approver queue are each a thin render over one call: submitRequest validates the justification against the declared prompts server-side regardless of what your form did, so a hand-rolled client cannot skip a required answer.
For the role editor and grant picker that sit alongside these, @alfiz/core ships the wildcard-aware permission tree (buildPermissionTree, toggleNode, isNodeChecked, isNodeIndeterminate, and nodePattern. It is selection logic and state, not markup: whole-group selection stores the <group>.* pattern, which is what makes forward-inclusion real rather than a snapshot.