Skip to main content
snapshot is the pattern for server-rendered frameworks and any code path that performs multiple authorization checks for the same principal. Instead of making a separate async call for each check, you call client.snapshot(principal) once, for a single provider round-trip, and then call .can(), .canAny(), .require(), and .requireAny() synchronously on the returned snapshot. Every check in the snapshot sees one consistent instant of the principal’s access data, which is a stronger per-request guarantee than calling can repeatedly against a TTL cache that may tick over mid-render.

client.snapshot(principal, options?)

PrincipalRef
required
Identifies who the snapshot is for. Either { userId: string } for a human user or { serviceId: string } for a machine principal.
readonly LooseScopeId<S>[]
Scope instances to pre-resolve for synchronous scoped checks. You only need to list scopes for hierarchical scope types, those whose catalog declaration has a non-null parent. Flat, top-level scope types (declared with parent: null) are resolved synchronously from the catalog without I/O, so you do not need to list them here. Scopes that appear in the principal’s own grant and revoke rows are always pre-resolved automatically.
boolean
default:"false"
When true, bypasses both the subject-closure cache and the object ancestor-chain cache, fetching fresh data from the provider. Equivalent to calling can.fresh for every check in the snapshot.
boolean
default:"true"
Whether checks made on this snapshot emit metrics observations. Inert unless the client was constructed with metrics. Snapshot checks are the render path’s conditional-UI traffic, the highest-volume shape there is, and what sampleRate.visibility exists for. Pass false to suppress the whole snapshot; view-as previews do this on the previewed subject’s side, so attribution never follows the preview.
Checking a hierarchical scope the snapshot has not resolved throws UnresolvedScopeError rather than evaluating a truncated chain, which would fail open on an ancestor revoke. Name those scopes in scopes, or extend the snapshot afterwards with await snap.resolve(scopes). The error carries the scope, its type, whether it is declared, and the scopes this snapshot can evaluate.

Return value

Promise<AlfizSnapshot>
A snapshot containing the compiled closure data and grant rows for the principal at the moment of the round-trip. Once resolved, all checks on the snapshot are synchronous.

When to use a snapshot

Use client.snapshot(principal) whenever a single request or render performs more than one check for the same principal:
  • RSC (React Server Component) renders that check multiple permissions in a single pass, inside .map() callbacks, conditional sections, and layout components.
  • List pages that render per-item action buttons (edit, delete, share) for every row in a result set.
  • Route loaders that guard the page and then conditionally load additional data based on permissions.
  • Any helper function that needs to call can without being async, pass the snapshot down instead of the client.
Use client.can / client.can.fresh directly when:
  • You need a single check and are not in a server render.
  • You are performing a destructive action and want fresh semantics for the entire snapshot (pass { fresh: true } to get this with a snapshot too).

Staleness and freshness

A snapshot is a point-in-time view of a principal’s access. It draws from the same caches as client.can, and a snapshot is one consistent instant of those caches, which is a stronger per-request guarantee (every check sees the same data), not a weaker one. For destructive actions within a render, do not re-use the snapshot. Use client.can.fresh(...) for the destructive gate, keeping the snapshot for read checks.
A snapshot captures the subject-closure data once. Do not reuse a snapshot across requests or background jobs. Take a fresh snapshot at the start of each request.

Examples

RSC page with multiple checks

List page with per-item action buttons

For large result sets (hundreds or thousands of rows), prefer pushing the filter into the database using client.grantedScopes rather than resolving a chain per row. snap.resolve is ideal for tens-of-rows cases or for enriching a snapshot mid-request with a small number of additional scopes.

Passing a snapshot to synchronous helpers

Fresh snapshot for just-in-time elevation