@talosjs/role package provides config-agnostic role-based access control (RBAC). You declare your roles and how they inherit one another in a single configuration, and the Role class answers one question at a time: does a user’s role grant a required role through the hierarchy? Roles attach to the user, routes declare which roles may reach them, and the runtime checks the two against the inheritance graph before a controller runs.
What the role package does
You define your own role names and hierarchy in YAML (or in code). The package ships defaults but imposes no fixed set. A roleinherits its ancestors, so granting ROLE_ADMIN automatically grants everything ROLE_USER can do, with no copy-pasting grants across roles. Role identifiers are Uppercase<string>, and generateRolesTypes turns your config into a literal union so typos fail at compile time. The whole thing is pure graph traversal with no runtime dependencies, so it works in the browser and on Bun. The roles field on a route plugs into Talos routing and the auth pipeline.
How it works
Authorization resolves against the inheritance graph. A user holds a role; a route requires one or more roles; access is granted when the user’s role is a required role or inherits it (directly or transitively). Siblings on different branches never satisfy each other.The roles.yml configuration
A config has two sections. roles maps short keys to their full ROLE_* identifiers; hierarchy describes each role’s inherits edges and a human description. Inheritance flows upward: a child lists the parents it absorbs.
roles.yml
rolesConfig:
Role naming convention
Role identifiers are uppercase and prefixed withROLE_, typed as Uppercase<string>. The same identifiers appear everywhere a role is referenced: in the hierarchy keys, in a user’s assigned roles, and in the roles array on a route. Routing declares the field as roles: Uppercase<string>[], so a protected route reads roles: ["ROLE_ADMIN", "ROLE_SUPER_ADMIN"]. Keeping the convention consistent is what lets the generated types catch a misspelled role.
The Role class
Role is the access-control engine. Construct it with no arguments and pass your config to each call; it holds no state.
IRole interface, so you can swap in your own implementation where one is expected.
Validating the config
validateConfig(config) enforces the contract before the config is trusted. It checks that the required role keys exist (GUEST, TRIAL_USER, USER, PREMIUM_USER, ADMIN, SUPER_ADMIN, SYSTEM), that every role maps to a hierarchy entry, that each entry has a non-empty description, and that every inherits target is itself defined. Any failure throws a RoleException. Run it once at startup or in a test so a broken config never ships.
Generating role types
generateRolesTypes(config) returns a string of TypeScript that turns your config into literal types: a RoleType union of role keys, a RoleHierarchyRoleType union of hierarchy roles, and a TypedRolesConfigType that ties them together. Writing this output to a .ts file gives you compile-time safety, so referencing a role that does not exist becomes a type error instead of a silent runtime miss.
roles.yml so the types stay in sync with the config.
Enforcing access on a route
A route declares the roles permitted to reach it through theroles field. The runtime resolves the request’s user, reads the user’s role, and grants access only when it satisfies one of the listed roles through the hierarchy. Listing ROLE_MANAGER, then, also admits ROLE_ADMIN and ROLE_SUPER_ADMIN. Roles answer who a user is; permissions answer what they may do, and the two compose.
roles sits alongside permission, env, ip, and host in the access-control checks.
RoleException
RoleException extends the framework Exception and is thrown for role failures, whether a malformed config or a denied check. It carries the offending role as its key and resolves to HTTP 403 Forbidden, so a denial surfaces as the correct status without extra mapping.
Designing a role hierarchy
Modelinherits edges instead of duplicating the same role across many routes, since granting an ancestor grants its descendants’ reach automatically. Keep role names stable: routes, users, and stored data all reference the ROLE_* identifiers, so renaming one is a breaking change across the system. Call validateConfig early (or in a test) so an undefined inherits target or missing description fails fast rather than at request time, and re-run generateRolesTypes whenever you edit roles.yml so a typo’d role is a compile error rather than a silent denial.
When guarding a route, name the lowest role that qualifies. Because higher roles inherit lower ones, the hierarchy then admits everyone above it. Use roles for broad identity tiers and permissions for fine-grained actions, and combine both on sensitive routes.