> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alfiz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Check Shapes: can, canAny, can.fresh, and require

> Alfiz exposes two check shapes: can() for gates and canAny() for visibility affordances, plus can.fresh() to bypass caches on destructive actions.

Every check in Alfiz flows through one of two shapes: `can` is the authorization gate, and `canAny` is the visibility affordance. A third form, `can.fresh`, carries the same signature as `can` but bypasses every cache. It exists because bounded staleness is acceptable on most reads but never on destructive writes. Understanding which shape to reach for, and when, is the core of using Alfiz correctly.

## `can(principal, key, scope?)`

`can` is the only valid gate shape. Call it before performing any state-changing operation or returning protected data.

```ts theme={null}
const allowed: boolean = await alfiz.can(
  { userId },           // PrincipalRef
  "docs.files.read",   // K, compile-time verified against the catalog
  "docs.doc:123",      // ScopeId (optional; omit for global checks)
);
```

**`principal`** is a `PrincipalRef`, a discriminated union:

```ts theme={null}
{ userId: string }      // a human user
{ serviceId: string }   // a service account
```

Pass the shape that matches your session: `{ userId: session.userId }` for user sessions, `{ serviceId: "export-worker" }` for machine callers.

**`key`** is a typed template-literal derived from your catalog. The TypeScript compiler rejects any string that is not a declared leaf. You can also pass a `readonly` array of keys, and `can` returns `true` when the principal holds *any one* of them:

```ts theme={null}
await alfiz.can({ userId }, ["docs.files.read", "docs.files.update_file"], "docs.doc:123");
```

**`scope`** is an optional `ScopeId`: the resource instance being accessed (e.g., `"docs.doc:123"`, `"docs.folder:9"`). Omit it for global checks. When a principal holds the key at any ancestor of the scope, `can` returns `true`, because hierarchy is resolved at check time.

`can` returns `Promise<boolean>`. It never throws on a denied check; use `require` when you want a throw instead.

<Note>
  Every call to `can` is verified against the catalog before evaluation. If the key is not declared, Alfiz throws `UnknownPermissionError`, a programming error and not a denial. Map it to a 500, never to a 403.
</Note>

***

## `can.fresh(principal, key, scope?)`

`can.fresh` carries exactly the same signature as `can` but bypasses all caches: subject closures are not read from the cache, and object ancestor chains are re-fetched from the provider.

```ts theme={null}
// Destructive action: always use can.fresh
const allowed = await alfiz.can.fresh(
  { userId },
  "docs.files.delete",
  "docs.folder:9",
);
```

**When to use `can.fresh`:**

* **Destructive actions.** Deleting, publishing, or transferring a resource where a stale allow would be worse than a brief latency hit.
* **Just-in-time elevations.** The principal was just granted emergency access and must be able to act on it immediately, without waiting for a cache TTL.

**Why it exists, and the staleness bounds:** Subject-side closures are cached for `subjectCacheTtlMs` (default 30 seconds). A revocation can therefore take up to 30 seconds to propagate to `can` responses. On most read paths that bound is acceptable. On destructive paths it is not, and `can.fresh` is the explicit escape hatch.

<Tip>
  Pair `can.fresh` with destructive server actions as a convention: `can.fresh` makes the staleness contract visible at the call site, so reviewers know the action is being explicit about freshness.
</Tip>

***

## `canAny(principal, pattern)`

`canAny` is the visibility affordance. It answers the question: does this principal hold *anything* matching `pattern`, at *any* scope? Use it to show or hide navigation items and section headings.

```ts theme={null}
const visible: boolean = await alfiz.canAny(
  { userId },
  "docs.*",   // PatternOf<typeof catalog>, a catalog-typed wildcard pattern
);
```

`pattern` is a wildcard string derived from the catalog's `$pattern` union, for example `"docs.*"` or `"docs.files.*"`. Passing an undeclared pattern throws `UnknownPermissionError`.

<Warning>
  **`canAny` is never a gate.** It tells you whether a section is worth showing. It does not authorize any action. The `alfiz-verify` static verifier emits an error whenever `canAny` appears inside a server action or route handler. Gate on a concrete key with `can` or `require`.
</Warning>

***

## `require` and `requireAny`

`require` and `requireAny` are the throwing forms of `can` and `canAny`. They return `Promise<void>` and throw `AccessDeniedError` when the check fails.

<CodeGroup>
  ```ts require theme={null}
  // Throws AccessDeniedError on denial
  await alfiz.require(
    { userId },
    "docs.files.update_file",
    "docs.doc:123",
  );
  ```

  ```ts requireAny theme={null}
  // Throws AccessDeniedError if the principal holds nothing under "docs.*"
  await alfiz.requireAny({ userId }, "docs.*");
  ```
</CodeGroup>

`AccessDeniedError` carries a typed `reason` field:

```ts theme={null}
import { AccessDeniedError } from "@alfiz/core";

try {
  await alfiz.require({ userId }, "docs.files.delete", "docs.folder:9");
} catch (err) {
  if (err instanceof AccessDeniedError) {
    // err.reason: "unauthenticated" | "forbidden" | "inactive"
    // err.permission: the key(s) that were checked
    // err.scope: the scope that was checked (if any)
    return redirect(err.reason === "unauthenticated" ? "/login" : "/403");
  }
  throw err;
}
```

Use `require` at the top of server actions and route handlers where you want a single thrown value to carry all downstream error handling. Use the boolean `can` when you need to branch without catching.

***

## `snapshot(principal, options?)`: one round-trip, synchronous checks

Server-rendered frameworks make hundreds of conditional-UI checks inside `.map()` callbacks and pure helpers that cannot be async. `snapshot` solves this: one provider round-trip, then every subsequent check is synchronous.

```ts theme={null}
// Once per request, one provider round-trip. Name the hierarchical scopes
// you intend to check, so their ancestor chains resolve now.
const snap = await alfiz.snapshot({ userId }, { scopes: ["docs.doc:123"] });

// Every check below is synchronous, so it is safe inside .map() and render helpers
snap.can("docs.files.read");                        // GLOBAL scope, not "anywhere"
snap.can("docs.files.update_file", "docs.doc:123"); // scoped, synchronous
snap.canAny("docs.*");                              // visibility affordance
snap.require("docs.files.read");                    // throws AccessDeniedError on deny
snap.holds("docs.files.delete");                    // held at ANY scope: unscoped UI hint
snap.heldKeys;                                      // ReadonlySet<PermissionKey>
```

A snapshot is a *stronger* consistency guarantee than repeated `can` calls: every check in a request sees one subject-data instant and one evaluation clock. The caches do not tick over mid-render.

**List pages.** When you do not know your row scope IDs until after the query:

```ts theme={null}
const snap = await alfiz.snapshot({ userId });
snap.require("docs.files.read");                    // gate the page

const rows = await db.docs.findMany({ ... });       // query now
await snap.resolve(rows.map((r) => `docs.doc:${r.id}`));

// Now synchronous per-row checks are safe
const editable = rows.filter((r) =>
  snap.can("docs.files.update_file", `docs.doc:${r.id}`)
);
```

Pass `{ fresh: true }` to bypass caches at snapshot time, the same way `can.fresh` does for individual checks.

```ts theme={null}
const snap = await alfiz.snapshot({ userId }, { fresh: true });
```

<Note>
  Flat scope types, meaning those declared `parent: null` **and** not `multiParent`, are always synchronously resolvable from a snapshot. Hierarchical scope types require pre-resolution, either via `client.snapshot(principal, { scopes: [...] })` up front, or `await snap.resolve([...])` once the IDs are known. Checking an unresolved hierarchical scope throws, rather than silently evaluating a truncated chain.
</Note>

***

## Staleness reference

| Cache layer            | Default TTL | Bust mechanism                                                                                                                                        |
| ---------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Subject closures       | 30 s        | Provider `user`, `subject`, `role`, `catalog`, or `all` invalidation event                                                                            |
| Object ancestor chains | 60 s        | Provider `scope` event for any scope in the chain, or a `catalog` / `all` event; `app.notifyScopeMoved(scope)` from your code emits the `scope` event |

`can.fresh` bypasses both layers. For moves your application makes in its own tables, call `app.notifyScopeMoved(scope)` from the same code path that changes the parent pointer. The TTL is the backstop and not the primary mechanism.
