Inheritance and default parents
A subject inherits the grants of every id listed in its parents, and those
parents inherit from their own parents, and so on. Inheritance is resolved at
evaluation time — nothing is copied or persisted.
await vperms.saveSubject("workspace", { id: "user", type: SubjectType.User, parents: ["developers"],});await vperms.saveSubject("workspace", { id: "developers", type: SubjectType.Group, parents: ["staff"],});await vperms.setPermission("workspace", "staff", "posts.read", true);
await vperms.can("workspace", "user", "posts.read"); // true (two levels up)Direct grants win first
Section titled “Direct grants win first”Evaluation is two-phase:
- Direct grants of the subject are matched first. If any direct grant matches, it decides the result and inheritance is not consulted.
- Otherwise the inherited grants are resolved and matched.
If neither phase matches, the built-in
vperms.subject.me.permissions grant is checked last.
await vperms.setPermission("workspace", "developers", "posts.read", true);await vperms.setPermission("workspace", "user", "posts.read", false);
await vperms.can("workspace", "user", "posts.read"); // false (direct deny wins)Parent layers
Section titled “Parent layers”Parents come from three sources, called layers. Lower is higher priority:
| Layer | Name | Source |
|---|---|---|
0 |
Explicit | subject.parents |
1 |
Type default | defaultParents.byType[type] |
2 |
Global default | defaultParents.global |
3 |
Built-in | the self-permissions grant |
The next layer is only consulted when the previous one produced no matching permission — not merely when it had no grants. Within a layer, closer parents take priority over more distant ancestors.
Virtual (default) parents
Section titled “Virtual (default) parents”defaultParents applies virtual parents to every subject the service
evaluates, without storing them:
const vperms = new VeguiPermsService({ adapter, defaultParents: { global: ["everyone"], byType: { user: ["users"], service: ["services"] }, },});Parents reached through a default are tagged with the worse of the two layers, so a default edge can never outrank an explicit edge. A parent reached through a default also contributes its own explicit parents and its own defaults.
Default parents are validated by DefaultParentsSchema. They cannot be
negation directives — opting out happens on the subject, not in configuration.
Negating a virtual parent
Section titled “Negating a virtual parent”Prefix an entry in parents with ! to opt the subject out of that virtual
parent:
await vperms.saveSubject("workspace", { id: "user", type: SubjectType.User, parents: ["!everyone"],});Negation behaves as follows:
- It only opts out of virtual parents (
byTypeandglobal), removing the id from both sources at once. - It never removes an explicit parent with the same id. If the subject also
lists
everyoneexplicitly, the explicit entry wins and inheritance still happens. - When declared on the evaluated subject, it is propagated through the whole walk: the negated id is never reached through a virtual source, not even when an intermediate parent would apply it as one of its own defaults.
- A parent that lists the negated id explicitly still reintroduces it.
The negation prefix is exported as VIRTUAL_PARENT_NEGATION ("!"), and
splitParents(parents) returns the explicit parents and the set of excluded
ids:
import { splitParents } from "vperms";
splitParents(["developers", "!everyone"]);// { explicit: ["developers"], excluded: Set { "everyone" } }Cycles and order independence
Section titled “Cycles and order independence”Inheritance uses a visited set, so cyclic parents (a → b → a) never cause
infinite recursion and duplicate paths are collapsed. The result does not
depend on the order of the parents array: every tiebreak is content-based.
import { effectiveParentLayers } from "vperms";
effectiveParentLayers( { id: "user", type: SubjectType.User, parents: ["developers", "!everyone"] }, { global: ["everyone"], byType: { user: ["users"] } },);// {// explicit: ["developers"],// byType: ["users"],// global: [],// }effectiveParentLayers(subject, defaults) returns the three layers already
deduplicated, with negation applied. Continue with
Resolution to see how the engine turns all of this
into a single effective permission set.