> ## 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.

# createAlfizClient: Initialize the Alfiz Client Instance

> createAlfizClient(options) creates a typed AlfizClient bound to a catalog and provider. All check methods are typed against the catalog's permission keys.

`createAlfizClient` is the single entry point for the evaluator. It binds a catalog and a provider together into an `AlfizClient` whose `can`, `canAny`, and `require*` methods are typed against the exact key and pattern unions your catalog declares, so every call site is verified at compile time, and every runtime-string path is verified before evaluation. You call this once at application startup and share the resulting client across requests.

## Signature

```typescript theme={null}
function createAlfizClient<Cat extends AnyCatalog>(
  options: Omit<AlfizClientOptions, "catalog"> & { catalog: Cat },
): AlfizClient<Cat["$key"], Cat["$pattern"], Cat["$scope"]>
```

The client carries **three** derived type parameters: the catalog's key union, its pattern union, and its scope-id union. `ClientOf<Cat>` is the alias for exactly that, so you can annotate a stored client without hand-writing any of them.

```typescript theme={null}
import { type ClientOf } from "@alfiz/core";
// ClientOf<typeof catalog> ≡ AlfizClient<
//   KeyOf<typeof catalog>, PatternOf<typeof catalog>, ScopeOf<typeof catalog>
// >
```

Keys and patterns **gate**: a typo at a literal call site is a compile error. Scope ids **hint** (`LooseScopeId`): a literal scope 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.

## Options

<ParamField body="catalog" type="AnyCatalog" required>
  The built catalog returned by `defineCatalog`. Every key and pattern passed
  to `can`, `canAny`, or any `require*` method is verified against this catalog
  before evaluation. Passing an undeclared key raises
  `UnknownPermissionError`, a programming error and not a denial.
</ParamField>

<ParamField body="provider" type="AlfizProvider" required>
  The provider implementation. In the open-source packages this is the
  `AlfizApplication` returned by `createApplication`; Alfiz Cloud speaks the
  same contract. The client attaches to the provider's invalidation stream on
  construction and detaches when you call `client.close()`.
</ParamField>

<ParamField body="subjectCacheTtlMs" type="number" default="30000">
  How long (in milliseconds) a principal's subject closure is cached before
  it is re-fetched from the provider. Subject closures are wide, high-churn
  data that tolerate seconds-to-minutes propagation delay. This is the
  documented staleness bound for revocation propagation; `can.fresh` is the
  escape hatch when you need an immediate check.
</ParamField>

<ParamField body="objectCacheTtlMs" type="number" default="60000">
  How long (in milliseconds) a scope's ancestor chain is cached. Object
  chains are narrow and near-static; provider `scope` invalidation events
  bust them immediately when a resource moves. This TTL bounds staleness only
  when a move was never reported via `notifyScopeMoved`.
</ParamField>

<ParamField body="maxSubjectCacheEntries" type="number" default="10000">
  The maximum number of principals kept in the subject cache. When this limit
  is exceeded, the least-recently-used entries are evicted. Tune this to
  match your active-user count, because eviction causes a re-fetch on the next
  check for the evicted principal.
</ParamField>

<ParamField body="maxObjectCacheEntries" type="number" default="10000">
  The maximum number of scope chains kept in the object cache. When this
  limit is exceeded, the least-recently-used entries are evicted. Tune this
  to match your active-scope working set.
</ParamField>

<ParamField body="revalidateAfterMs" type="number | false" default="5000 (with an epoch)">
  Cross-process revalidation window in milliseconds. Against a provider
  that persists its invalidation log (the Application's `events.persist`,
  itself on by default since 0.7.0), the client validates its caches with
  one constant-cost read of the log head per window, shared across every
  concurrent check. An unchanged head renews TTLs; a changed head replays
  only the missed events through the same busting logic the live stream
  feeds. Effective cross-process staleness becomes this window rather than
  the blind TTL. Fail-closed: an unreachable epoch degrades to the
  pre-epoch TTL contract, never below it.

  **On by default (5 000 ms) when the provider exposes an epoch**. The
  safe configuration is the default one since 0.7.0. Pass a number to
  choose the window, or `false` to opt out and return to TTL-only caching.
  Inert against a provider with no epoch.
</ParamField>

<ParamField body="strict" type="boolean" default="false">
  The incident switch: `true` makes **every** check on every surface bypass
  both cache tiers, taking a fresh closure supply and fresh ancestry, per check.
  exactly as if each call were `can.fresh`. Propagation bounds collapse to
  zero; every check pays provider round-trips. Wire it to an environment
  variable and flip it during a security incident, then flip it back. See
  the [incident runbook](/enforcement/caching#the-incident-switch).
</ParamField>

<ParamField body="cacheStore" type="CacheStore">
  Optional shared cache tier (L2) between the in-process caches and the
  provider. Read order is L1 → L2 → provider. The win is cold processes
  (serverless invocations, fresh deploys) finding a warm closure instead of
  paying the full fan-out. Entries are served only when provably fresh.
  under the current event-log head with `revalidateAfterMs` on, or within
  `cacheStoreTtlMs` without it, and every L2 failure is a miss, never an
  answer. Writes are fire-and-forget. The store holds closure data inside
  the server trust boundary: point it only at authenticated, private cache
  infrastructure. Use `respCacheStore(client)` to adapt any RESP-family
  client (node-redis or ioredis call shape, so Redis, Valkey, KeyDB,
  Dragonfly, ElastiCache, Upstash) with no added dependency.
</ParamField>

<ParamField body="cacheKeyPrefix" type="string" default="alfiz:v1:">
  Key prefix for L2 entries. Change this only if a single cache instance is
  shared across incompatible Alfiz deployments.
</ParamField>

<ParamField body="cacheStoreTtlMs" type="number">
  Storage TTL for L2 entries. With `revalidateAfterMs` set, this only
  bounds storage growth (freshness comes from the sequence check) and
  defaults to 10 minutes. Without an epoch it *is* the freshness bound, and
  defaults to a single value, the larger of the two L1 TTLs,
  `Math.max(subjectCacheTtlMs, objectCacheTtlMs)`, so 60 000 ms at the
  defaults.
</ParamField>

<ParamField body="onCacheStoreError" type="(error: unknown) => void">
  Observes swallowed L2 errors for metrics and logging. Failures are misses
  either way.
</ParamField>

<ParamField body="metrics" type="MetricsOptions">
  Emit a structured `CheckObservation` per evaluated check, carrying shape, decision,
  permission, scope type, principal, and the rows that decided it, to an
  observer you supply. Off by default. Point it at
  `otelMetricsObserver({ meter })`, `createMetricsAggregator()`, or
  `createProviderMetricsSink(app)`. Observations are built only after the
  `sampleRate` draw passes, observers are invoked fire-and-forget, and an
  observer that throws can never fail a check. See
  [Permission metrics](/api/metrics).
</ParamField>

<ParamField body="externalPermissions" type="&#x22;error&#x22; | &#x22;warn&#x22; | &#x22;allow&#x22;" default="&#x22;error&#x22;">
  What to do with a check for a permission in a namespace this catalog neither owns nor [imports](/catalog/imports), an *implicit* import. `"error"` throws `UnknownPermissionError`, as it always has; `"warn"` evaluates it and reports once per distinct permission; `"allow"` evaluates it silently.

  Two cases never soften, whatever you set: a permission under a namespace you **own** (your catalog is enumerable, so an unknown key in it is a typo) and one outside an **enumerated** import (that import knows its own keys, and says which). And nothing here performs I/O: no provider lookup, no fetch. `snapshot.can()` is synchronous, which is what makes the decision structurally local rather than merely conventionally so.

  A permission admitted this way is declared in no catalog, so a bare global `*` grant does not confer it; a grant naming the namespace does.
</ParamField>

<ParamField body="onExternalPermission" type="(info: ExternalPermissionInfo) => void">
  Called once per distinct permission admitted under `externalPermissions: "warn"`. Replaces the default `console.warn`; point it at your logger or your metrics. Checks admitted this way also carry `externalPermission: true` in the metrics observation stream, so you can count what still needs declaring.
</ParamField>

<ParamField body="clock" type="() => number">
  Optional. A function that returns the current time as epoch milliseconds.
  Defaults to `Date.now`. Override this in tests to control grant expiry
  without manipulating wall time.
</ParamField>

## Return value

<ResponseField name="AlfizClient<K, P, S>" type="object">
  A fully initialized client. The type parameters `K`, `P`, and `S` are the
  catalog's derived key, pattern, and scope-id unions, inferred at the
  `createAlfizClient` call site. Store this as `ClientOf<typeof catalog>` on
  your application context.

  <Expandable title="Members">
    <ResponseField name="can" type="CanFn<K, S>">
      The primary authorization gate: `can(principal, key, scope?, options?)`,
      returning `Promise<boolean>`. **No scope means the global scope**.
      strictest question, not "anywhere". Also exposes `can.fresh(...)` for
      cache-bypassing checks.
    </ResponseField>

    <ResponseField name="canAny" type="(principal, pattern: P) => Promise<boolean>">
      The visibility affordance. Tests whether the principal holds any
      permission matching a pattern. Never use as a gate.
    </ResponseField>

    <ResponseField name="require" type="(principal, key: K | readonly K[], scope?) => Promise<void>">
      Throwing form of `can`. Throws `AccessDeniedError` on denial.
    </ResponseField>

    <ResponseField name="requireAny" type="(principal, pattern: P) => Promise<void>">
      Throwing form of `canAny`. Throws `AccessDeniedError` when no matching
      permission is held.
    </ResponseField>

    <ResponseField name="snapshot" type="(principal, options?) => Promise<AlfizSnapshot<K, P, S>>">
      Fetches all closure data in one round-trip, returning a snapshot whose
      `can` / `canAny` / `require` / `holds` / `heldKeys` are synchronous.
    </ResponseField>

    <ResponseField name="explain" type="(principal, key, scope?) => Promise<CheckExplanation & ...>">
      Returns the full explanation of an authorization decision, including
      matching grants and suppressing revokes.
    </ResponseField>

    <ResponseField name="grantedScopes" type="(principal, key) => Promise<{ granted: Set<ScopeId>, revoked: Set<ScopeId> }>">
      The listing primitive: returns the scopes at which a principal holds a
      permission.
    </ResponseField>

    <ResponseField name="holds" type="(principal, key, options?) => Promise<boolean>">
      Returns `true` if the principal holds the key at **any** scope. A
      visibility affordance, never a gate. `alfiz-verify` errors on a `holds`
      call inside a server action or route handler.
    </ResponseField>

    <ResponseField name="heldKeys" type="(principal, options?) => Promise<PermissionKey[]>">
      Returns every catalog key the principal holds at some scope. O(catalog)
      Call once per request, or take a snapshot and read `snap.heldKeys`.
    </ResponseField>

    <ResponseField name="close" type="() => void">
      Detaches from the provider's invalidation stream and clears all caches.
      Call this on server shutdown to avoid leaked subscriptions.
    </ResponseField>
  </Expandable>
</ResponseField>

## `ClientOf<Cat>`: the stored-client type alias

When you attach the client to a framework context object or export it from a module, annotate it with `ClientOf` rather than spelling out the full generic:

```typescript theme={null}
import { type ClientOf } from "@alfiz/core";
import { catalog } from "./catalog.js";

// ✅ Clean annotation for context objects
type AppContext = {
  alfiz: ClientOf<typeof catalog>;
};
```

## `client.close()`

```typescript theme={null}
client.close(): void
```

Unsubscribes from the provider's invalidation stream, clears the subject and object caches, and cancels any in-flight fetches. Call this when the server is shutting down, or when you hot-reload the client after a catalog republish. There is no closed flag: a check made after `close()` still evaluates correctly, it has simply lost event-driven invalidation and falls back to TTL-only freshness. Construct a new client rather than relying on that.

## Complete initialization example

```typescript theme={null}
import { defineCatalog, group, createAlfizClient } from "@alfiz/core";
import { createApplication } from "@alfiz/application";
import { prismaDriver } from "@alfiz/prisma";

// 1. Define the catalog (once, at the module level).
export const catalog = defineCatalog({
  namespaces: ["docs"],
  permissions: group("docs.files", { scopes: ["docs.folder", "docs.doc"] }, {
    "docs.files.read":        true,
    "docs.files.update_file": true,
    "docs.files.delete":      { destructive: true, scopes: ["docs.folder"] },
  }),
  scopeTypes: {
    "docs.folder": { parent: null },
    "docs.doc":    { parent: "docs.folder" },
  },
});

// 2. Construct the provider, wiring in storage and the ancestry resolver.
//    `ancestry` returns the chain nearest-first, EXCLUDING the scope itself.
const app = createApplication({
  catalog,
  storage: prismaDriver(prisma),
  ancestry: async (scope) => db.ancestorChainOf(scope),
});

// 3. Create the client and export it for use across the application.
export const alfiz = createAlfizClient({
  catalog,
  provider: app,
  subjectCacheTtlMs: 30_000,
  objectCacheTtlMs:  60_000,
});

// 4. On server shutdown, detach cleanly.
process.on("SIGTERM", () => {
  alfiz.close();
});
```

<Note>
  `createAlfizClient` attaches to the provider's invalidation stream
  immediately on construction. The stream delivers cache-bust events whenever
  a grant, membership, or scope move is written through the provider, so you do
  not need to manage cache invalidation manually.
</Note>

## Cross-process caching (multi-node and serverless)

The in-process invalidation stream only reaches the process that wrote the event, so in a multi-node or serverless deployment the TTLs alone would bound how quickly a revocation on one instance takes effect on another. `revalidateAfterMs` tightens that bound to a window by validating the caches against the durable event log the Application maintains, and since 0.7.0 **both halves are on by default**: the Application persists events whenever its driver can, and the client revalidates whenever its provider exposes an epoch. The example below spells the options out anyway; against the bundled drivers it is what you get with no options at all.

Turn on the log on the Application, then set a revalidation window on the client:

```typescript theme={null}
import { createApplication } from "@alfiz/application";
import { createAlfizClient, respCacheStore } from "@alfiz/core";
import { createClient } from "redis";

const app = createApplication({
  catalog,
  storage: prismaDriver(prisma),
  ancestry,
  events: { persist: true }, // durable invalidation log (the default with this driver)
});

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

export const alfiz = createAlfizClient({
  catalog,
  provider: app,
  revalidateAfterMs: 5_000,        // cross-process staleness bound
  cacheStore: respCacheStore(redis), // shared L2 for cold starts
});
```

With this configured:

* **Long-lived nodes** validate caches once per window with a single-row read whose cost is independent of organization size. Quiet systems stop refetching entirely (TTLs are renewed); a write anywhere propagates within one window.
* **Serverless invocations** find warm closures in the shared L2 instead of paying the full closure fan-out on every cold start. Entries are only served when the L2 stamp matches the current log head.

Both features are opt-in and independent. See the Application's `events.persist` option for the write-side setup.
