Skip to content

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 }],
// }

Grants are sorted with compareGrants, highest priority first:

  1. Layer — explicit parents, then type defaults, then global defaults, then built-in (lower wins).
  2. Depth — closer parents before more distant ancestors.
  3. Specificity — more specific pattern before broader wildcard.
  4. Exact segments — more exact segments before fewer.
  5. Deny before allow — an explicit deny wins an exact tie.
  6. 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.

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) - index

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

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"); // false
canResolved(permissions, "workspaces.2.read"); // true
canResolved(permissions, "posts.read"); // false

canResolved 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.

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.