defineCatalog(), and from that moment every permission key in your codebase is verified at compile time against the shape you described. There is no dashboard to drift, and no runtime surprise from a misspelled key.
What the catalog contains
The catalog is not an enum of strings. It is a structured description of four things:- Permissions. Every leaf action and read your application exposes, organized into named groups.
- Scope types. The resource hierarchy your permissions apply to (folders, documents, projects, or nothing at all for flat apps).
- Navigation entries. The menu structure your UI renders, wired to permission patterns so items hide automatically for users who hold nothing under them.
- Namespaces. The ownership prefixes that scope every key to your application, preventing collisions when multiple permission catalogs are composed together. Permissions your application references but does not own, whether from Alfiz Cloud or another application, are declared separately as imports.
Keys and namespaces
A permission key is a dotted string. Only one thing about its shape is structural: the first segment must be a namespace your application owns, declared innamespaces. Everything after that is yours to organize.
zoom.host and a four-segment one are both perfectly ordinary catalogs.
conventions: { depth } is a house-style setting rather than a rule of the system. It defaults to 3, which is what most application catalogs land on naturally, but it is one line to change and lintCatalog is the only thing that reads it. Set { depth: 2 }, { depth: 4 }, or { depth: "any" } to match how you already name things.defineCatalog(): function signature and minimal example
defineCatalog accepts a CatalogInput object and returns a typed Catalog<C> instance. It throws a CatalogError at boot when the input is structurally invalid, so a broken catalog fails immediately, before the first check.
defineCatalog throws on:
- Invalid key segments (segments must match
/^[a-zA-Z][a-zA-Z0-9_]*$/) - Single-segment keys (the first segment is the namespace, and a namespace is a group)
- A key that is also a group path, since
docs.filesdeclared alongsidedocs.files.readwould be both a folder and a leaf - Duplicate permission keys
- Keys outside the declared namespaces
- References to undeclared scope types
lintCatalog and surfaced in CI by alfiz-verify, never at boot. The split is deliberate: what is structurally broken fails at boot, and what is merely off-convention is a CI finding you can configure or switch off.
Derived template-literal types
When you pass a literal object todefineCatalog, TypeScript derives a union of every valid key from your input shape. Those unions flow into the typed properties catalog.$key and catalog.$pattern, and Alfiz’s check methods are typed against them:
KeyOf and PatternOf to type function parameters that receive permission keys or patterns at runtime.
Emitting the catalog for alfiz-verify
alfiz-verify reads your catalog from a JSON snapshot rather than importing your source module directly. Emit the snapshot with:
--experimental-strip-types needs Node 22.6 or newer. On an older runtime, compile first and run the emitter against the built output.toDocument() returns a CatalogDocument, a serializable snapshot of the catalog, which alfiz-verify reads to check coverage, gate shapes, and naming conventions. Add this step to your CI pipeline before running the verifier.
The
alfiz_internal.* permissions (access administration, access requests, audit log, and catalog management) are included in every catalog by default. Set includeAlfizInternal: false in your CatalogInput only when your application renders no Alfiz administration surface.What’s next
Permissions
Keys, kinds, wildcards, and the naming floor
Scope Types
Resource hierarchies and per-type grantability
Navigation Wiring
Menu visibility tied to real permissions