Skip to main content

Roles & Inheritance

Roles aggregate permissions into named sets and support DAG-based inheritance with automatic permission flattening.

Creating Roles

import { createRole } from "@hex-di/guard";

const ViewerRole = createRole("Viewer", {
permissions: [ReadUsers],
});

const EditorRole = createRole("Editor", {
permissions: [WriteUsers],
inherits: [ViewerRole], // inherits ReadUsers from Viewer
});

const AdminRole = createRole("Admin", {
permissions: [DeleteUsers],
inherits: [EditorRole], // inherits ReadUsers + WriteUsers
});

Role Inheritance DAG

Roles form a directed acyclic graph (DAG) through the inherits option. When a role inherits from another, it automatically receives all of that role's permissions -- including permissions from further ancestors.

Permission Flattening

Permissions flatten automatically through the inheritance chain. Each role's effective permissions include its directly declared permissions plus all permissions inherited from ancestors.

RoleDirect PermissionsFlattened Permissions
ViewerReadUsersReadUsers
EditorWriteUsersWriteUsers, ReadUsers
AdminDeleteUsersDeleteUsers, WriteUsers, ReadUsers

Flattening happens at construction time, not at evaluation time. This means hasRole(AdminRole) checks are O(1) -- the flattened permission set is precomputed.

Diamond Inheritance

Multiple inheritance paths to the same ancestor are handled correctly. Permissions are deduplicated during flattening.

const ViewerRole = createRole("Viewer", {
permissions: [ReadUsers],
});

const EditorRole = createRole("Editor", {
permissions: [WriteUsers],
inherits: [ViewerRole],
});

const ModeratorRole = createRole("Moderator", {
permissions: [ModerateUsers],
inherits: [ViewerRole],
});

// Admin inherits from both Editor and Moderator
// ReadUsers appears once in the flattened set (deduplicated)
const AdminRole = createRole("Admin", {
permissions: [DeleteUsers],
inherits: [EditorRole, ModeratorRole],
});

Admin's flattened permissions: DeleteUsers, WriteUsers, ModerateUsers, ReadUsers -- no duplicates despite two paths to Viewer.

Cycle Detection

Circular inheritance is detected at construction time. If role A inherits from B and B inherits from A (directly or transitively), createRole returns a CircularRoleInheritanceError.

// This would be caught at construction time:
const RoleA = createRole("A", { permissions: [], inherits: [RoleB] });
const RoleB = createRole("B", { permissions: [], inherits: [RoleA] });
// -> CircularRoleInheritanceError

Cycle detection runs during createRole, not at evaluation time. Invalid role graphs fail fast.