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

# defineCatalog(): Build Your Alfiz Permission Tree

> defineCatalog(input) builds a typed catalog: the source of truth for permission keys, scope types, navigation, and compile-time KeyOf/PatternOf unions.

`defineCatalog` is where your authorization model lives. It takes a structured declaration of your permission tree and produces a `Catalog` object that the Application validates writes against, the client checks keys against, and the build-time verifier consumes. The catalog is declared in code, never configured in a dashboard and never inferred from call sites, so it is always the ground truth.

Permissions are declared by their **full dotted key**, the same notation every check, grant, role pattern, and nav entry uses, and group levels are inferred from those keys. A small catalog is one flat map; a large one composes from [`group()` blocks](/catalog/permissions#organizing-a-large-catalog).

`defineCatalog` throws `CatalogError` immediately on structural invalidity (bad segments, undeclared namespaces, missing scope types, a key that is also a group path). Convention violations such as key depth, the naming floor and nav wiring are reported by `alfiz-verify` at build time, not at runtime.

## Signature

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

function defineCatalog<const C extends CatalogInput>(input: C): Catalog<C>
```

## Parameters

<ParamField body="namespaces" type="readonly string[]" required>
  The namespaces your application owns, being the first segment of every permission key it declares. The first entry is the primary namespace. Each must be a single valid segment (`[a-zA-Z][a-zA-Z0-9_]*`); `alfiz_internal` is reserved.

  Catalogs are federation-shaped from the first commit, so this is required even standalone, where it is locally redundant. Example: `["docs"]` produces keys like `docs.files.read`; a multi-project portal declares `["docs", "billing", "admin"]`.

  This is what your application **owns**. Permissions it merely references belong in `imports`.
</ParamField>

<ParamField body="imports" type="Record<string, ImportInput>" optional>
  Permissions your application references but does not own, keyed by the foreign namespace, whether from Alfiz Cloud or a federated application. See [Imports](/catalog/imports) for the full treatment.

  ```ts theme={null}
  imports: {
    zoom: {
      from: "registry:zoom@^3",
      document: zoomDoc,            // the owner's published catalog, committed
      scopes: ["docs.folder"],      // YOUR scope types, never the owner's
      permissions: { "zoom.host": true, "zoom.meetings.*": true },
    },
  }
  ```

  Each import declares specific keys or subtree patterns, never the bare `*`. Importing a namespace you own, declaring an entry outside the namespace it names, or wiring one to a scope type this catalog does not declare all throw `CatalogError`. With a `document` attached, an entry matching nothing the owner publishes throws too, which is the local half of drift detection.

  `strict: true` closes the admission half of an undocumented wildcard: without it, an **opaque region** admits any key matching its pattern, since there is no published document to check the key against. Set it when you import a wildcard you have no document for and still want a typo to fail.
</ParamField>

<ParamField body="permissions" type="PermissionsInput" required>
  The permissions, keyed by their **full dotted key**. Accepts a flat map, a single `group()` block, or an array mixing both:

  ```ts theme={null}
  // A small catalog: one flat map, no groups needed.
  permissions: {
    "docs.files.read": { kind: "read" },
    "docs.files.delete": { destructive: true },
  }

  // A larger one: named blocks, composable across files.
  permissions: [files, folders, { "docs.reports.read": { kind: "read" } }]
  ```

  Group levels are inferred from the keys, so every dotted prefix of a key is a group. Values are `true` (all defaults) or a `PermissionLeafInput`. See [Catalog Types](/api/catalog-types) for the full field reference.
</ParamField>

<ParamField body="groups" type="Record<string, GroupInput>" optional>
  Metadata (`label`, `description`, `scopes`) for group paths you did not declare a `group()` block for, typically the project level, e.g. `{ docs: { label: "Documents" } }`. Purely optional: an undecorated group falls back to its path segment.
</ParamField>

<ParamField body="scopeTypes" type="Record<string, ScopeTypeInput>" optional>
  Scope types this catalog uses. Required for any catalog that grants permissions at resource scopes (anything other than global `"*"`). Keys are dotted identifiers like `"docs.folder"` or `"docs.doc"`, and the first segment must be one of the catalog's declared `namespaces`, or `defineCatalog` throws `CatalogError`. A bare `"folder"` is rejected for the same reason a bare permission key is.

  See [Catalog Types](/api/catalog-types) for the full `ScopeTypeInput` field reference.
</ParamField>

<ParamField body="navigation" type="NavItemInput[]" optional>
  Sidebar / nav tree wiring. Each item declares a `permission` (a key, array of keys, or subtree pattern) that controls visibility of that nav item. The `alfiz-verify` linter checks that all referenced permissions exist in the catalog.

  See [Catalog Types](/api/catalog-types) for the full `NavItemInput` field reference.
</ParamField>

<ParamField body="conventions" type="{ depth?: number | &#x22;any&#x22; }" optional>
  House conventions the linter enforces. `depth` is the key depth `lintCatalog` checks for, defaulting to `3`; set `2` for a thin integration catalog (`zoom.host`), another number for a deeper tree, or `"any"` to opt out.

  This is a **naming preference, not a rule of the system**. Nothing about evaluation, grants, patterns, or types reads it. `lintCatalog` is the only consumer, and a deviation is a CI finding rather than a boot-time throw. Set it to match your keys instead of renaming them to match it.
</ParamField>

<ParamField body="includeAlfizInternal" type="boolean" default="true" optional>
  When `true` (the default), Alfiz's own administration permissions are included under `alfiz_internal.*`. Set to `false` only for catalogs that render no Alfiz admin UI surface. When `false`, view-as and all other `alfiz_internal.*` features are unavailable, so `createSession` will deny any view-as attempt because the permission key does not exist.
</ParamField>

## Return value

<ResponseField name="Catalog" type="Catalog<C>">
  A `Catalog` object carrying:

  * `catalog.leaves`, a `ReadonlyMap<string, LeafMeta>` of all permission keys
  * `catalog.groups`, a `ReadonlyMap<string, GroupMeta>` of all group paths
  * `catalog.scopeTypes`, a `ReadonlyMap<string, ScopeTypeMeta>`
  * `catalog.navigation`, the built nav tree
  * `catalog.namespace` / `catalog.namespaces`
  * `catalog.toDocument()` emits the JSON wire shape for `alfiz-verify`

  The phantom fields `catalog.$key` and `catalog.$pattern` carry the derived `CatalogKeys<C>` and `CatalogPatterns<C>` union types. Extract them with `KeyOf<typeof catalog>` and `PatternOf<typeof catalog>`.
</ResponseField>

## `catalog.toDocument()`

`toDocument()` serializes the catalog to the stable `CatalogDocument` wire shape consumed by `alfiz-verify` and the provider's catalog publish endpoint.

```bash theme={null}
# Emit to a file for use by alfiz-verify:
node --experimental-strip-types -e \
  'import("./src/alfiz.ts").then(m => console.log(JSON.stringify(m.catalog.toDocument())))' \
  > alfiz-catalog.json
```

<Note>
  `--experimental-strip-types` needs **Node 22.6 or newer**. On an older runtime, compile first and run the emitter against the built output.
</Note>

## Template-literal types

Importing the catalog and using `KeyOf` / `PatternOf` gives you compile-time verified permission strings everywhere:

```ts theme={null}
import type { KeyOf, PatternOf } from "@alfiz/core";
import { catalog } from "./alfiz.js";

type AppKey = KeyOf<typeof catalog>;
// → "docs.files.read" | "docs.files.update_file" | "docs.files.delete" | "alfiz_internal.access.read" | ...

type AppPattern = PatternOf<typeof catalog>;
// → "*" | AppKey | "docs.*" | "docs.files.*" | "alfiz_internal.*" | ...

// Type a function that accepts any valid permission key:
declare function requirePermission(key: AppKey, scope?: string): Promise<void>;

// Type a variable holding a pattern:
const visibilityPattern: AppPattern = "docs.files.*";
```

## `includeAlfizInternal: true`: built-in admin permissions

By default, `defineCatalog` merges the `alfiz_internal` project into the catalog. This adds the following permission keys, which gate Alfiz's own headless administration components:

| Key                                      | Description                                          |
| ---------------------------------------- | ---------------------------------------------------- |
| `alfiz_internal.access.read`             | View the access administration surface               |
| `alfiz_internal.access.manage_roles`     | Create, edit, delete roles                           |
| `alfiz_internal.access.manage_groups`    | Create, edit, delete user groups and their parentage |
| `alfiz_internal.access.manage_grants`    | Create and delete grants                             |
| `alfiz_internal.access.manage_revokes`   | Create and delete personal revokes                   |
| `alfiz_internal.access.manage_reporting` | Edit reporting (manager) edges                       |
| `alfiz_internal.access.view_as`          | Preview the product as a role or an individual       |
| `alfiz_internal.requests.read`           | View the approvals inbox                             |
| `alfiz_internal.requests.decide_request` | Approve or deny an access request                    |
| `alfiz_internal.audit.read`              | Read the audit log                                   |
| `alfiz_internal.catalog.read`            | View the published catalog                           |
| `alfiz_internal.catalog.publish_catalog` | Publish a verified catalog to the provider           |

<Note>
  `alfiz_internal.*` keys are exempt from coverage linting in `alfiz-verify`, because they are gated inside Alfiz's own admin surfaces, not your application code, so unreferenced warnings are suppressed for them automatically.
</Note>

## Complete example

A document management application with two scope types, a multi-group project, navigation wiring, and template-literal types:

```ts theme={null}
import { defineCatalog, group } from "@alfiz/core";
import type { KeyOf, PatternOf } from "@alfiz/core";

// Each block is a named, foldable unit. In a real app these typically live
// in their own files, next to the code they gate, and are imported here.
const files = group("docs.files", {
  label: "Files",
  description: "Read and modify documents and folders",
  scopes: ["docs.folder", "docs.doc"],
}, {
  "docs.files.read": {
    label: "View",
    description: "View document contents and folder listings",
    kind: "read",
    impliedOnAncestors: true,
  },
  "docs.files.update_file": {
    label: "Edit",
    description: "Edit document content or rename a folder",
  },
  "docs.files.share": {
    label: "Share",
    description: "Invite collaborators and change sharing settings",
  },
  "docs.files.delete": {
    label: "Delete",
    description: "Permanently delete a document or folder",
    destructive: true,
  },
});

const admin = group("docs.admin", {
  label: "Administration",
  description: "Workspace-level administrative actions",
}, {
  "docs.admin.read": {
    label: "View admin panel",
    kind: "read",
  },
  "docs.admin.manage_members": {
    label: "Manage members",
    description: "Invite, remove, and manage workspace members",
  },
  "docs.admin.manage_billing": {
    label: "Manage billing",
    description: "Update payment methods and subscription",
  },
});

export const catalog = defineCatalog({
  namespaces: ["docs"],
  groups: { docs: { label: "Documents", description: "Document and folder access" } },
  permissions: [files, admin],

  scopeTypes: {
    "docs.folder": {
      description: "A folder in the document hierarchy",
      parent: null,                  // top-level: direct children of *
      requestable: {
        prompts: [
          { id: "reason", label: "Why do you need access?", required: true },
        ],
        requireExpiry: true,
        maxDurationMs: 7 * 24 * 60 * 60 * 1000, // 7 days max
        policy: {
          stages: [{ kind: "management", layers: 1 }],
        },
      },
    },
    "docs.doc": {
      description: "An individual document",
      parent: "docs.folder",         // documents live inside folders
    },
  },

  navigation: [
    {
      label: "Documents",
      href: "/docs",
      permission: "docs.files.read",
      children: [
        {
          label: "My Files",
          href: "/docs/mine",
          permission: "docs.files.read",
        },
        {
          label: "Shared",
          href: "/docs/shared",
          permission: "docs.files.read",
        },
      ],
    },
    {
      label: "Admin",
      href: "/admin",
      permission: "docs.admin.read",
    },
  ],
});

// Derived types. Use these throughout your application:
export type AppKey = KeyOf<typeof catalog>;
export type AppPattern = PatternOf<typeof catalog>;
```
