FEATURES / RBAC

A permission matrix
TypeScript can read.

Roles defined as a single typed object. Every guarded endpoint checks against it. ESLint flags missing handlers when you add a new role or action.

Roles + actions, in one file.

Typed permission matrix

Roles × actions, stored as a Record<Role, Set<Action>>. Discriminated unions everywhere.

Guard middleware

requireAction(actor, "billing:cancel") throws ForbiddenError. The HTTP layer translates to 403.

Resource ownership

Combine role checks with ownership predicates: "members can edit their own profile, admins anyone's".

UI mirroring

Server returns the actor's permission set; the dashboard hides actions the user can't take. No phantom buttons.

Adding a role is a 5-line PR.

Add a role to the union, fill in the permission cells. ESLint flags every consumer that needs to handle it. The build won't pass until the matrix is complete.

modules/iam/domain/rbac.ts
 1  export type Role = "owner" | "admin" | "member" | "viewer";
 2  
 3  export const PERMISSIONS: Record<Role, Set<Action>> = {
 4    owner:  new Set(["*"]),
 5    admin:  new Set(["billing:read", "users:invite", ...]),
 6    member: new Set(["projects:read", "projects:write"]),
 7    viewer: new Set(["projects:read"]),
 8  };

Authorization without YAML.

A typed matrix the compiler can verify, not a config file.