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

# Alfiz Sessions: createSession and View-As Preview

> createSession() makes a typed per-request session from a verified user ID. Covers view-as admin preview, service principals, and cookie serialization.

A session is the typed, per-request object your server code calls `can()` on. It carries an authenticated `actorUserId` and, optionally, a view-as state for admin preview. Sessions are the bridge between your identity provider (Clerk, Auth.js, or any other) and Alfiz's permission evaluation. Alfiz does not issue or validate tokens; you verify the token through your provider and pass the resulting user ID to `createSession`.

## Creating a session

```ts theme={null}
import { createSession } from "@alfiz/application";

async function createSession<K extends string, P extends string, S extends string>(
  client: AlfizClient<K, P, S>,
  options: SessionOptions,
): Promise<AlfizSession<K, P, S>>
```

### `SessionOptions`

<ParamField body="actorUserId" type="string" required>
  The ID of the authenticated user, as issued by your identity provider. This is the user whose **real** permissions are checked. It is never replaced by a view-as state for access decisions.
</ParamField>

<ParamField body="viewAs" type="ViewAsState | null" optional>
  Activates a preview session. Pass `null` (or omit) for a normal session. Activating a preview is gated for you: when `viewAs` is non-null, `createSession` calls `assertCanViewAs` and throws `AccessDeniedError` unless the **actor** holds `alfiz_internal.access.view_as`. A catalog built with `includeAlfizInternal: false` declares no such key, so previews fail closed rather than erroring.

  ```ts theme={null}
  type ViewAsState =
    | { kind: "role"; roleId: string }
    | { kind: "user"; userId: string }
  ```
</ParamField>

### Return value

<ResponseField name="AlfizSession" type="AlfizSession<K, P, S>">
  A session object with `can`, `canAny`, `require`, and `requireAny` methods. All checks are preview-narrowed: a view-as session can only see access that both the actor **and** the previewed subject hold. A preview can narrow what is shown; it can never escalate privileges.
</ResponseField>

## Session methods

```ts theme={null}
class AlfizSession<K, P, S> {
  readonly actorUserId: string;
  readonly viewAs: ViewAsState | null;
  readonly subjectUserId: string;    // preview user's id during user preview, actor's otherwise

  can(key: K | readonly K[], scope?: LooseScopeId<S>): Promise<boolean>
  canAny(pattern: P): Promise<boolean>
  require(key: K | readonly K[], scope?: LooseScopeId<S>): Promise<void>  // throws AccessDeniedError
  requireAny(pattern: P): Promise<void>                                   // throws AccessDeniedError

  snapshot(options?: SnapshotOptions<S>): Promise<AlfizSessionSnapshot<K, P, S>>
}
```

Every check on a session intersects the actor's access with the previewed
subject's, so a preview can only ever **narrow**. It can never show the viewer
a surface their own grants would not reach.

<Note>
  Always use `actorUserId` for audit attribution. It is never the previewed subject. Use `subjectUserId` for data-scoped surfaces (e.g., "show documents owned by this user"), and it returns the previewed user's ID during a user preview, and the actor's ID otherwise.
</Note>

## `session.snapshot()`: the render path under view-as

The async session methods are fine for a gate, but a render path cannot `await`
inside `.map()`. `session.snapshot()` is the session-shaped version of the
[per-request snapshot](/api/snapshot): **one fetch per identity**, then
synchronous, preview-narrowed checks.

```ts theme={null}
const snap = await session.snapshot({ scopes: [`docs.folder:${folderId}`] });

snap.can("docs.files.update_file", `docs.doc:${docId}`); // sync
snap.canAny("docs.files.*");                             // sync
snap.holds("docs.files.share");                          // sync, never a gate
snap.heldKeys;                                           // ReadonlySet<PermissionKey>
snap.require("docs.files.read", `docs.doc:${docId}`);    // throws, sync

snap.actorUserId;    // always the real actor
snap.subjectUserId;  // the previewed user during a user preview
snap.at;             // the instant this snapshot represents

await snap.resolve(rows.map((r) => `docs.doc:${r.id}`)); // extends BOTH identities
```

`options`, meaning pre-resolved `scopes` and `fresh`, applies to both identities at
once, so the actor and the preview are always read at the same instant.

<Note>
  A role preview evaluates synchronously with no extra resolution, because role
  patterns are global-scope by construction. An unknown role id fails closed.
</Note>

<Warning>
  Denials thrown from a session snapshot name the **actor**, not the previewed
  identity, so attribution never follows the preview. The same is true of metrics:
  an administrator looking through someone's eyes did not use that person's
  grants, so the previewed side is recorded with `observe: false`.
</Warning>

Annotate a stored session snapshot with `SessionSnapshotOf<typeof catalog>`,
which completes the derived-type family alongside `ClientOf`, `SnapshotOf`, and
`SessionOf`.

## Derived session type

Use `SessionOf<typeof catalog>` to type session variables without writing out the `K`, `P`, and `S` type parameters by hand:

```ts theme={null}
import type { SessionOf } from "@alfiz/application";
import { catalog } from "./alfiz.js";

type AppSession = SessionOf<typeof catalog>;
// equivalent to AlfizSession<KeyOf<typeof catalog>, PatternOf<typeof catalog>>
```

## Wiring to Clerk (Next.js example)

Alfiz does not provide a Clerk adapter package. You write a thin `getAlfizSession` helper that calls Clerk's `auth()` and passes the user ID to `createSession`. This keeps the integration explicit and avoids a hard dependency on any specific Clerk version.

```ts theme={null}
// lib/alfiz-session.ts
import { auth } from "@clerk/nextjs/server";
import { createSession, parseViewAs } from "@alfiz/application";
import { alfiz } from "./alfiz.js"; // your AlfizClient

export async function getAlfizSession() {
  const { userId } = await auth();
  if (!userId) return null;

  // View-as state is persisted in a cookie set by your admin UI.
  // Parse it server-side; createSession gates on the real actor's permissions.
  const { cookies } = await import("next/headers");
  const jar = await cookies();
  const viewAsRaw = jar.get("alfiz_view_as")?.value ?? null;
  const viewAs = parseViewAs(viewAsRaw);

  return createSession(alfiz, { actorUserId: userId, viewAs });
}
```

```ts theme={null}
// app/actions/publish-doc.ts
"use server";

import { getAlfizSession } from "@/lib/alfiz-session";

export async function publishDoc(docId: string) {
  const session = await getAlfizSession();
  if (!session) throw new Error("Unauthenticated");

  await session.require("docs.files.publish", `docs.doc:${docId}`);
  // ... publish logic
}
```

## View-as: admin preview

Administrators holding `alfiz_internal.access.view_as` can preview the product as either a role or an individual user. `createSession` enforces this gate automatically, so passing a non-null `viewAs` without the permission throws `AccessDeniedError`.

```ts theme={null}
import { createSession, serializeViewAs, parseViewAs } from "@alfiz/application";

// Start a role preview (UI sets the cookie):
const session = await createSession(alfiz, {
  actorUserId: adminUserId,
  viewAs: { kind: "role", roleId: "role-id-here" },
});

// Start a user preview:
const session = await createSession(alfiz, {
  actorUserId: adminUserId,
  viewAs: { kind: "user", userId: "target-user-id" },
});

// End the preview (no permission check, since stopping a preview is always allowed):
const session = await createSession(alfiz, {
  actorUserId: adminUserId,
  viewAs: null,
});
```

Serialize and deserialize view-as state for cookie storage:

```ts theme={null}
import { serializeViewAs, parseViewAs } from "@alfiz/application";

// Serialize to a cookie value:
const cookieValue = serializeViewAs({ kind: "role", roleId: "r1" });
// → "role:r1"

// Deserialize from a cookie value:
const viewAs = parseViewAs(cookieValue);
// → { kind: "role", roleId: "r1" }
```

<Warning>
  Stopping a preview is deliberately ungated (anti-lockout). Never require `view_as` to exit a preview session. Build the "exit preview" button to call `createSession` with `viewAs: null`.
</Warning>

## `assertCanViewAs`

If you need to check preview eligibility independently (for example, to show or hide the "view as" UI affordance), call `assertCanViewAs` directly:

```ts theme={null}
import { assertCanViewAs } from "@alfiz/application";

async function assertCanViewAs<K extends string, P extends string, S extends string>(
  client: AlfizClient<K, P, S>,
  actorUserId: string,
): Promise<void>
```

Throws `AccessDeniedError` when the actor does not hold `alfiz_internal.access.view_as`, or when the catalog was built with `includeAlfizInternal: false`.

## Service principals

For machine-to-machine authentication in background jobs, CI pipelines and internal services, Alfiz provides a local service-key shim that validates shared bearer tokens server-side.

### Creating the shim

```ts theme={null}
import {
  createServiceKeyShim,
  parseServiceKeysEnv,
} from "@alfiz/application";

// Parse keys from the conventional env format:
// ALFIZ_SERVICE_KEYS="worker:key1current,key1previous;scheduler:key2"
const configs = parseServiceKeysEnv(process.env.ALFIZ_SERVICE_KEYS);
const serviceKeys = createServiceKeyShim(configs);
```

### Verifying a service request

```ts theme={null}
// In a route handler or middleware:
const result = serviceKeys.verify(request.headers.get("Authorization"));
if (!result.ok) {
  return Response.json({ error: "Unauthorized" }, { status: 401 });
}

// result.principal is a PrincipalRef, so pass it to can() like any user:
const allowed = await alfiz.can(result.principal, "docs.files.read");
```

A verified service key yields the machine subject `service:<serviceId>`. Grant to it like any other subject:

```ts theme={null}
await app.createGrant({
  subject: "service:worker",
  pattern: "docs.files.*",
  provenance: { kind: "admin", actorUserId: "root" },
});
```

### The env format

`parseServiceKeysEnv` parses `ALFIZ_SERVICE_KEYS` in the format:

```
<serviceId>:<currentKey>[,<previousKey>];<serviceId2>:<key>;...
```

Keep the previous key present during rotation, then remove it after confirming the new key is deployed everywhere.

### Key length requirement

Keys must be at least 16 characters. `createServiceKeyShim` throws at startup if any key is shorter.

### Client-reach guard

Service keys must never reach the browser. Add `ALFIZ_SERVICE_KEYS` to `forbidClientIdentifiers` in your `alfiz-verify.config.json` so the build fails if the key module ever becomes client-reachable:

```json theme={null}
{
  "forbidClientIdentifiers": ["ALFIZ_SERVICE_KEYS"]
}
```

See [Verify Config](/api/verify-config) for the full configuration reference.
