Roles
A role is a named, reusable set of permission patterns. Roles carry no negative patterns, since the only negative layer in Alfiz is the personal revoke. The interface that describes a role’s core shape isRoleDef:
id, so renaming a role never breaks any existing grant. An admin can change name and description freely; the grants that reference the role’s id continue to work.
Creating a role
Useapp.createRole(input, provenance) to define a new role. Supply a caller-chosen id when something outside the runtime must reference it by a stable identifier (e.g. a SQL migration seeding a well-known role). Omit id for a generated one.
app.updateRole(roleId, input, provenance) to rename or re-describe a role, or to change its patterns. app.deleteRole(roleId, provenance) refuses while the role is still in use: it rejects with code: "conflict" naming the number of grants that confer it, and again for any pending request that references it. Remove the grants and decide or cancel the requests first.
Assigning a role
A role grant is an ordinary grant row: the samecreateGrant call you use for raw patterns, with roleId instead of pattern:
patterns and evaluates each one against the requested permission key. Granting a role gives the subject every pattern the role carries, including patterns added to the role in future updates, because grants reference the role id, not a snapshot of its patterns.
A grant carries exactly one of
roleId or pattern, never both and never neither. Alfiz validates this at write time and rejects invalid grant rows before touching storage.Groups
A group is a named cohort of users that can be granted access in one row. TheUserGroup interface is:
id. Renaming a group never breaks grants or memberships.
Creating and managing groups
groupIds: string[], and setGroupMembership overwrites that array wholesale after de-duplicating it. At subject-closure computation time, every group in the list, and every ancestor of those groups, joins the closure.
Group nesting (the inheritance DAG)
A group may declare parent groups. It inherits the union of all its parents’ access. The parent graph must be a DAG, and Alfiz enforces this transactionally at everysetGroupParents call. Attempting to create a cycle rejects the write with a ProviderWriteRejectedError whose code is "graph_cycle" and whose message names the full path:
Groups never revoke
Groups can only widen access, and never narrow it. The only mechanism for restricting a specific user’s access is a personal revoke on that individual. This constraint is intentional: union-only inheritance is simple, predictable, and avoids the class of bugs that arise when “deny” semantics on one group override “allow” semantics on another.Virtual parents
When several groups should all receive exactly the same access, create a virtual parent group and grant access to it. Each child group declares the virtual parent as a parent, and inherits its grants through the normal DAG walk:Dissolving a virtual parent
When a virtual parent is no longer needed, callapp.dissolveVirtualParent(groupId, provenance). Dissolution is a snapshot: every unexpired grant on the virtual parent is copied down to each child group with provenance: { kind: "dissolution", ... }, after which the parent is deleted and the children drift freely:
DirectoryImportResult.warnings. The semantics are correct, because a cycle expresses “these groups are effectively one pool”, and you can dissolve the virtual parents manually afterward.
Role-based visibility with canAny
Use alfiz.canAny(principal, pattern) to check whether a user holds any permission matching a wildcard pattern. This is the right tool for deciding whether to show an admin section, a management toolbar, or a restricted menu item. It never gates a concrete action:
Built-in alfiz_internal permissions
Alfiz’s own administration surface is gated by a reserved namespace, alfiz_internal, that is added to every catalog automatically. These permissions gate the headless admin components that ship with Alfiz:
Grant these to your admin group with the same
createGrant call used for any other permission:
alfiz_internal namespace is reserved and cannot be used in your own catalog definition. It is added automatically and will never collide with your application’s permission tree.