Resolution and precedence
Resolution is the process of turning the direct and inherited grants of a
subject into a single, ordered list of effective permissions. It is what makes
can() deterministic and what clients evaluate locally.
const resolved = await vperms.resolvePermissions("workspace", "user");// {// id: "user",// type: "user",// parents: ["developers"],// permissions: [{ permission: "workspaces.1.read", value: true, weight: 100 }],// }Precedence order
Section titled “Precedence order”Grants are sorted with compareGrants, highest priority first:
- Layer — explicit parents, then type defaults, then global defaults, then built-in (lower wins).
- Depth — closer parents before more distant ancestors.
- Specificity — more specific pattern before broader wildcard.
- Exact segments — more exact segments before fewer.
- Deny before allow — an explicit deny wins an exact tie.
- Pattern, then subject id — stable, content-based tiebreak.
Because every tiebreak is content-based, the result is independent of the order
of the parents array and of grant insertion order.
Deduplication and weights
Section titled “Deduplication and weights”Once ordered, grants are deduplicated by permission, keeping the highest-priority occurrence. Each remaining permission is then assigned a weight that decreases with its position:
weight = (number of effective permissions) - indexThe highest-priority permission receives the largest weight. weight is
computed on the fly and never persisted. It folds source layer, inheritance
depth and pattern specificity into a single comparable number.
Evaluating a resolved subject
Section titled “Evaluating a resolved subject”canResolved(permissions, permission) re-evaluates a permission using only the
DTO: among all matching entries, the one with the highest weight wins.
import { canResolved } from "vperms";
const permissions = [ { permission: "workspaces.*", value: true, weight: 1 }, { permission: "workspaces.1.*", value: false, weight: 2 },];
canResolved(permissions, "workspaces.1.read"); // falsecanResolved(permissions, "workspaces.2.read"); // truecanResolved(permissions, "posts.read"); // falsecanResolved is guaranteed to agree with server-side can(), because both use
the same matcher and the same precedence. This is the mechanism behind the
browser client and
React bindings: they ship the JSON snapshot and need
no adapter, no inheritance and no evaluation on the server.
Manual resolution
Section titled “Manual resolution”Lower-level building blocks are exported from @vperms/core:
import { resolveInheritedPermissions, resolveSubjectPermissions,} from "@vperms/core";
// Only the inherited grants, tagged with `depth` and `layer`.const inherited = await resolveInheritedPermissions(adapter, "workspace", subject);
// Direct + inherited + built-in, deduplicated and weighted.const effective = await resolveSubjectPermissions(adapter, "workspace", subject);ResolvedPermissionGrant extends PermissionGrant with depth and layer,
so you can inspect where a permission came from before it is flattened into a
ResolvedSubject.
Next steps
Section titled “Next steps”- Guides: the service —
can(),resolvePermissions()and the request runtime. - Guides: exporting — exposing the DTO over HTTP.
- Adapters — persisting subjects and grants.