Skip to main content
Roles and groups are the two organizing abstractions that keep your grant table from becoming one row per user per permission. A role names a bundle of permission patterns that belong together; a group names a cohort of users that should receive access together. Assigning a role to a group is a single grant row, and every member of that group picks it up through their subject closure automatically.

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 is RoleDef:
Role definitions stored by the provider extend this with requestability configuration:
Role identity is the opaque 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

Use app.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.
Use 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 same createGrant call you use for raw patterns, with roleId instead of pattern:
At check time, Alfiz expands the role’s 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. The UserGroup interface is:
Like roles, a group’s identity is its opaque id. Renaming a group never breaks grants or memberships.

Creating and managing groups

Group membership is stored on the user record as 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 every setGroupParents call. Attempting to create a cycle rejects the write with a ProviderWriteRejectedError whose code is "graph_cycle" and whose message names the full path:
Cycle detection is transactional but not automatically serialized across concurrent writes. The storage driver’s runExclusive("groups", ...) method serializes graph writes per key. If you implement a custom storage driver, you must guarantee this serialization. Two individually-safe edge insertions can jointly form a cycle.

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, call app.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:
Directory imports that produce cyclic group graphs are auto-condensed: each strongly connected component collapses into a virtual parent automatically, with a warning returned in 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:
canAny is a visibility affordance only. Never use it as an authorization gate. Every page and action must still call alfiz.can(principal, key, scope) or alfiz.require(...) with a concrete key. The static verifier (alfiz-verify) will error on canAny used in a gate position.

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:
The 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.