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

# The Four Enforcement Points Every Application Surface Needs

> Alfiz requires checks at four points (page, navigation, server action, and conditional UI) to fully protect every surface in your application.

A surface is not done until all four enforcement points hold. This is the rule that `alfiz-verify` checks at build time and the rule that makes Alfiz's convention document reliable for both human reviewers and coding agents. Skipping any one point leaves a real gap: a user can reach the page but not load it, or load it but submit a hidden form, or navigate without seeing the item but still hit the API directly. All four must hold together.

## The four points

<Steps>
  ### Point 1: the page

  Gate every page handler or server component at the top, before any data fetching. Use `require` with the concrete key for the page's primary action. For project root pages, where the right question is "does this user have *anything* under this project?", use `requireAny` to gate on the subtree pattern, but still call `require` for the page's own read:

  ```ts theme={null}
  // app/(portal)/drive/[id]/page.tsx
  export default async function DocumentPage({ params }: { params: { id: string } }) {
    const session = await getSession();
    const scope = `docs.doc:${params.id}` as const;

    // Gate the page. Throws AccessDeniedError → redirected by your error boundary
    await alfiz.require({ userId: session.userId }, "docs.files.read", scope);

    const doc = await db.docs.findUniqueOrThrow({ where: { id: params.id } });
    // ...
  }
  ```

  For a project index page that lists all documents the user can access, gate on the subtree pattern:

  ```ts theme={null}
  // Project root: gate visibility, then gate the read
  await alfiz.requireAny({ userId: session.userId }, "docs.*");
  ```

  ### Point 2: navigation visibility

  Evaluate each nav item's `permission` field from the catalog via `canAny` before rendering. Items whose pattern the principal does not match do not render at all: not "greyed out", not hidden with CSS, simply not emitted.

  ```ts theme={null}
  // Layout or navigation component
  const session = await getSession();
  const principal = { userId: session.userId };

  const navItems = await Promise.all(
    catalog.navigation.map(async (item) => ({
      ...item,
      // `permission` is a pattern OR an any-of array of keys. `canAny` takes a
      // single pattern, so fan the array out and OR the results.
      visible: Array.isArray(item.permission)
        ? (await Promise.all(item.permission.map((k) => alfiz.holds(principal, k))))
            .some(Boolean)
        : await alfiz.canAny(principal, item.permission),
    }))
  );

  // Render only the items the user can see
  const visibleItems = navItems.filter((item) => item.visible);
  ```

  <Note>
    `canAny` is the right shape for a nav item wired to a **pattern**; `holds` is the right shape for one wired to a concrete key. Do not use `can` for nav visibility: its scope argument is optional, but omitting it makes it the *global* check, asking "may they do this everywhere?", which hides the item from everyone whose access is scoped, exactly the users nav visibility exists for. And do not use `canAny` or `holds` as a gate anywhere else.
  </Note>

  ### Point 3: server action or route handler

  Every server action and every route handler gates on a concrete permission before doing any work. This is the point that `alfiz-verify` enforces as an error (not a warning) on any exported async function in a `"use server"` file that contains no gate.

  <Tabs>
    <Tab title="Server action">
      ```ts theme={null}
      // app/actions/documents.ts
      "use server";

      import { alfiz } from "@/lib/alfiz";
      import { getSession } from "@/lib/session";

      export async function updateDocument(
        docId: string,
        updates: Partial<Doc>,
      ) {
        const session = await getSession();
        const scope = `docs.doc:${docId}` as const;

        // Gate before any work
        await alfiz.require(
          { userId: session.userId },
          "docs.files.update_file",
          scope,
        );

        return db.docs.update({ where: { id: docId }, data: updates });
      }

      export async function deleteDocument(docId: string) {
        const session = await getSession();
        const scope = `docs.doc:${docId}` as const;

        // Destructive: bypass caches
        const allowed = await alfiz.can.fresh(
          { userId: session.userId },
          "docs.files.delete",
          scope,
        );
        if (!allowed) throw new AccessDeniedError({ reason: "forbidden", permission: "docs.files.delete", scope });

        return db.docs.delete({ where: { id: docId } });
      }
      ```
    </Tab>

    <Tab title="Route handler">
      ```ts theme={null}
      // app/api/docs/[id]/route.ts
      import { alfiz } from "@/lib/alfiz";
      import { getSession } from "@/lib/session";
      import { isAccessDenied } from "@alfiz/core";
      import { NextResponse } from "next/server";

      export async function PUT(
        request: Request,
        { params }: { params: { id: string } },
      ) {
        const session = await getSession();
        const scope = `docs.doc:${params.id}` as const;

        try {
          await alfiz.require(
            { userId: session.userId },
            "docs.files.update_file",
            scope,
          );
        } catch (err) {
          if (isAccessDenied(err)) {
            return NextResponse.json({ error: err.reason }, { status: 403 });
          }
          throw err;
        }

        const body = await request.json();
        const doc = await db.docs.update({ where: { id: params.id }, data: body });
        return NextResponse.json(doc);
      }
      ```
    </Tab>
  </Tabs>

  When a server action bundles multiple field mutations, gate each field path on its own permission. A user who can update title but not move a document to a different folder should not be able to smuggle a `folderId` change through the same action.

  ### Point 4: conditional UI

  Gate every button, panel, menu item, and interactive element the user can see. Use `can` (or the snapshot's `.can()`) with the concrete scope of the resource being rendered. This is distinct from navigation visibility (Point 2): navigation is about showing sections; conditional UI is about showing controls within a page the user has already been allowed to reach.

  ```ts theme={null}
  // Document detail page, using a snapshot for synchronous per-row checks
  const snap = await alfiz.snapshot({ userId: session.userId }, { scopes: [scope] });
  snap.require("docs.files.read", scope); // Point 1: gate the page

  // Point 4: conditional UI controls
  const canEdit    = snap.can("docs.files.update_file", scope);
  const canDelete  = snap.can("docs.files.delete", scope);
  const canShare   = snap.can("docs.files.share", scope);
  ```

  ```tsx theme={null}
  // In the component
  {canEdit   && <EditButton docId={doc.id} />}
  {canDelete && <DeleteButton docId={doc.id} />}
  {canShare  && <SharePanel docId={doc.id} />}
  ```

  For unscoped conditional UI, meaning buttons that should appear whenever the user *holds* the permission at any scope, use `snap.holds(key)` rather than `snap.can(key)` without a scope. An editor who can update documents only in specific folders should still see the Edit button when it is relevant:

  ```ts theme={null}
  const editButtonExists = snap.holds("docs.files.update_file");
  // The button itself is still gated by the server action at the concrete scope
  ```
</Steps>

***

## Complete example: a document editor

The following shows all four points implemented for a single document-editor feature.

<CodeGroup>
  ```ts Page (Point 1) theme={null}
  // app/(portal)/docs/[id]/page.tsx
  import { alfiz } from "@/lib/alfiz";
  import { getSession } from "@/lib/session";
  import { DocumentEditor } from "./DocumentEditor";

  export default async function DocPage({ params }: { params: { id: string } }) {
    const session = await getSession();
    const scope = `docs.doc:${params.id}` as const;

    // Point 1: gate the page
    const snap = await alfiz.snapshot(
      { userId: session.userId },
      { scopes: [scope] },
    );
    snap.require("docs.files.read", scope);

    const doc = await db.docs.findUniqueOrThrow({ where: { id: params.id } });

    // Point 4: conditional UI resolved from the snapshot
    const canEdit   = snap.can("docs.files.update_file", scope);
    const canDelete = snap.can("docs.files.delete", scope);

    return (
      <DocumentEditor
        doc={doc}
        canEdit={canEdit}
        canDelete={canDelete}
      />
    );
  }
  ```

  ```ts Navigation (Point 2) theme={null}
  // app/(portal)/layout.tsx
  import { alfiz } from "@/lib/alfiz";
  import { getSession } from "@/lib/session";
  import { NavLink } from "@/components/NavLink";

  export default async function PortalLayout({ children }) {
    const session = await getSession();
    const principal = { userId: session.userId };

    // Point 2: navigation visibility
    const showDocs    = await alfiz.canAny(principal, "docs.*");
    const showAdmin   = await alfiz.canAny(principal, "admin.*");

    return (
      <div>
        <nav>
          {showDocs  && <NavLink href="/docs">Documents</NavLink>}
          {showAdmin && <NavLink href="/admin">Admin</NavLink>}
        </nav>
        {children}
      </div>
    );
  }
  ```

  ```ts Server action (Point 3) theme={null}
  // app/actions/documents.ts
  "use server";

  import { alfiz } from "@/lib/alfiz";
  import { getSession } from "@/lib/session";
  import { AccessDeniedError } from "@alfiz/core";

  export async function saveDocument(docId: string, data: { title: string; body: string }) {
    const session = await getSession();
    const scope = `docs.doc:${docId}` as const;

    // Point 3: gate the action before any work
    await alfiz.require(
      { userId: session.userId },
      "docs.files.update_file",
      scope,
    );

    return db.docs.update({ where: { id: docId }, data });
  }

  export async function deleteDocument(docId: string) {
    const session = await getSession();
    const scope = `docs.doc:${docId}` as const;

    // Point 3: destructive: bypass caches
    if (!(await alfiz.can.fresh({ userId: session.userId }, "docs.files.delete", scope))) {
      throw new AccessDeniedError({ reason: "forbidden", permission: "docs.files.delete", scope });
    }

    return db.docs.delete({ where: { id: docId } });
  }
  ```

  ```tsx Conditional UI (Point 4) theme={null}
  // app/(portal)/docs/[id]/DocumentEditor.tsx
  import { SaveButton } from "@/components/SaveButton";
  import { DeleteButton } from "@/components/DeleteButton";

  interface Props {
    doc: Doc;
    canEdit: boolean;
    canDelete: boolean;
  }

  export function DocumentEditor({ doc, canEdit, canDelete }: Props) {
    return (
      <article>
        <h1>{doc.title}</h1>
        <div>{doc.body}</div>
        <footer>
          {/* Point 4: render controls only when the check passed */}
          {canEdit   && <SaveButton docId={doc.id} />}
          {canDelete && <DeleteButton docId={doc.id} />}
        </footer>
      </article>
    );
  }
  ```
</CodeGroup>

***

## Scoped forms

All four points accept a `scope` argument wherever a concrete resource instance is available:

| Point          | Shape                            | Scope                                          |
| -------------- | -------------------------------- | ---------------------------------------------- |
| Page           | `snap.require(key, scope)`       | The resource being rendered                    |
| Navigation     | `canAny(principal, pattern)`     | Not applicable, since visibility is scope-free |
| Server action  | `require(principal, key, scope)` | The resource being mutated                     |
| Conditional UI | `snap.can(key, scope)`           | The resource being displayed                   |

For surfaces that operate globally (admin dashboards, org-level settings), omit the scope argument, because a global grant satisfies every scoped check by definition.

<Note>
  `alfiz-verify` does not have a rule per point. What it enforces is Point 3 directly: `ungated-action` fires on an exported async function in a server file with no gate, plus `visibility-as-gate` anywhere in a server file, `unknown-pattern` at every literal call site, and `unreferenced-leaf` across the catalog. Points 1 and 4 have no dedicated rule: a page or a button with no check is caught indirectly, as the `unreferenced-leaf` warning on the key nothing references. See [Static Verification](/enforcement/static-verification).
</Note>
