Skip to main content
A subject is anything that can appear on the left-hand side of a grant row. When Alfiz evaluates a permission check, it does not ask “does this user have a grant?”. It asks “does any member of this user’s subject closure have a grant?” That distinction is what makes groups, organizations, hierarchical group inheritance, and public access all work through the same single grant-row mechanism.

Subject kinds

Subject identities are typed string encodings. Every subject id encodes its kind as a prefix so it can live in grant rows, indexes, and wire formats without a parallel object model: Helper constructors produce the correctly-encoded ids:

The subject closure

When Alfiz evaluates a check for a user, it first computes the subject closure, the complete transitive set of subjects that user belongs to. The function that computes it takes a plain data input and is a pure function with no I/O:
The result is deterministic and includes, in this order:
  1. user:<userId>, the user themselves
  2. Every explicit group they belong to (group:<id>), breadth-first
  3. Every ancestor of those groups, transitively breadth-first
  4. Implicit reporting groups: directs:<directManager> and orgof:<m> for every manager in the chain
  5. Every organization they belong to (org:<id>)
  6. everyone
A concrete example: if Alice belongs to group:editors, and group:editors has parent group:staff, Alice’s subject closure is:
Any grant on group:staff reaches Alice automatically, with no direct grant needed.

everyone is not special-cased

The everyone subject is the mechanism for public or default access. “Anyone can read the public docs” needs no special flag and no mode switch. It is an ordinary grant row:
Because everyone is present in every subject closure, including that of unauthenticated or not-yet-provisioned users, this grant reaches all of them through the standard evaluation path. No special-casing is needed in the check engine.

Groups

Groups are named bundles of subjects. You grant access to a group with the same createGrant call you use for a user or any other subject:
Every member of group:editors now holds docs.files.update_file at that scope through their subject closure. Group membership is stored on the user record as groupIds: string[]. Manage membership with app.setGroupMembership(userId, groupIds, provenance):
Groups only widen access, and never revoke. The only negative layer in Alfiz is the personal revoke, which lives on individual users. That constraint is deliberate.

Group nesting

A group may declare parent groups. It inherits the union of all its ancestors’ access. The parent relationship is a DAG, and Alfiz enforces acyclicity transactionally at every edge write. Attempts to insert a cycle reject the write with a ProviderWriteRejectedError whose code is "graph_cycle", naming the full path:
The UserGroup interface that backs groups in storage is:
Group identity is the opaque id, so renaming a group with app.updateGroup(...) never breaks any existing grant.

Reporting edges and implicit groups

When you store reporting edges (userId → managerUserId) with app.setReportingEdge(...), Alfiz derives two implicit group subjects for every manager automatically:
  • directs:<managerUserId>. Everyone whose reportsTo edge points directly at that manager
  • orgof:<managerUserId>. The full transitive report tree under that manager
These implicit subjects can be granted to just like any other subject:
The membership of directs:manager_jane is computed dynamically from the reporting tree, so there is no separate membership list to maintain. When an employee’s manager changes, the implicit group membership updates automatically and the affected subject closures are invalidated.

Cache dynamics

Subject closures are cached with a default ~30-second TTL. Event-driven invalidation busts the cache immediately on:
  • Group membership changes (setGroupMembership)
  • Group parentage changes (setGroupParents)
  • Reporting edge changes (setReportingEdge)
  • User activation/deactivation (setUserActive)
  • Grant or revoke writes that touch a user-subject
The TTL is the upper bound on over-access after a revocation when an invalidation event is not available (for example, in a multi-instance deployment where the event did not propagate). For sensitive changes, use can.fresh(principal, key, scope) to bypass the cache entirely.

Showing a user’s subject closure

Call alfiz.explain(principal, key, scope) to see the full picture for a specific check, including which grants and revokes matched:
The underlying SubjectAccessData, reachable through app.getSubjectAccess(principal), contains the raw closure array, which is the full list of subject ids the check engine evaluated against: