alfiz-verify everything it needs to confirm that nav visibility and page-level gates are consistent, and you keep the permission-to-UI mapping in one place instead of scattered across layout files.
How navigation visibility works
Navigation visibility is evaluated bycanAny(). When a user loads your application, each nav item’s permission field is tested against what the user actually holds. Items for which the user holds nothing under the declared pattern simply do not render.
This evaluation happens on your server, like every other check. canAny reads your own database through the provider, and the synchronous form is snapshot.canAny, taken once per request. What makes it a display optimization rather than a security gate is what it answers: whether the user holds anything under the pattern. The page the nav item points to must gate itself independently with require on a concrete key.
NavItemInput fields
string
required
The text shown in the navigation menu. Also used as the path identifier in lint error messages, so keep it unique and descriptive.
string
The URL this nav item links to. Optional on parent items that are purely containers for children.
PermissionPattern | readonly PermissionKey[]
The visibility condition. Accepts three forms:
- A concrete key, such as
"docs.files.read". The item renders when the user holds exactly this permission. - An array of keys, such as
["docs.files.read", "docs.files.update_file"]. The item renders when the user holds any key in the list. - A wildcard pattern, such as
"docs.*"or"docs.files.*". The item renders when the user holds any permission under the pattern. This is the most common form for top-level nav sections.
alfiz-verify checks that every value here resolves against the catalog, so unknown keys and patterns that match nothing are lint errors.readonly NavItemInput[]
Nested nav items. Child visibility is evaluated independently, so a child can render even when its parent pattern would not match, though in practice parent items are almost always broader patterns that cover their children.
canAny vs. can
Why two methods?
Why two methods?
can is a gate. It returns a yes/no decision about a concrete, enumerated permission at a specific scope. Use it at every enforcement point: page guards, server actions, and route handlers.canAny is a visibility affordance. It returns true when the user holds anything under a pattern. It does not name what they can do, only whether there is something. Use it exclusively for rendering decisions: show this nav section, show this sidebar category, show this menu group.The rule is architectural: can is provable at a specific path in your codebase; canAny is not. alfiz-verify enforces this distinction by erroring when canAny appears in a server action or route handler.Expressing “show if the user holds anything under a group”
The most common pattern for sidebar sections is a wildcard over the project or tab:A complete multi-section navigation example
What alfiz-verify checks
When you run alfiz-verify, it lints every permission value in your navigation:
- Unknown key or pattern. A nav permission that doesn’t appear in the catalog is an error. This catches typos and references to permissions you renamed.
- Pattern matching no keys. A wildcard that currently matches nothing is a warning (the catalog may be growing and the pattern is intentional, but you should confirm).
canAnyused as a gate. Any file wherecanAnyguards a server action or exported handler is an error.