RBAC Domain
The rbac domain provides role-based access control (RBAC) for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.
Install
Section titled “Install”npx @backcap/cli add rbacDomain Model
Section titled “Domain Model”Role Entity
Section titled “Role Entity”The Role entity is the aggregate root of the RBAC domain. It holds the role’s identity, name, description, and a list of permissions.
import { Role } from "./domains/rbac/domain/entities/role.entity";import { Permission } from "./domains/rbac/domain/entities/permission.entity";
const perm = Permission.create({ id: crypto.randomUUID(), action: "read", resource: "posts",}).unwrap();
const result = Role.create({ id: crypto.randomUUID(), name: "editor", description: "Can read and edit posts", permissions: [perm],});
if (result.isOk()) { const role = result.unwrap(); console.log(role.name, role.permissions.length);}| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (UUID) |
name | string | Role name (must not be empty) |
description | string | Human-readable description |
permissions | Permission[] | List of granted permissions |
createdAt | Date | Set on creation |
updatedAt | Date | Set on creation, updated on mutation |
Role.create() returns Result<Role, InvalidRoleName>. If the name is empty, the result is a failure.
role.addPermission(perm) and role.removePermission(id) return new Role instances (immutable).
Permission Entity
Section titled “Permission Entity”import { Permission } from "./domains/rbac/domain/entities/permission.entity";
const result = Permission.create({ id: crypto.randomUUID(), action: "manage", resource: "posts", conditions: { ownOnly: true },});// Result<Permission, Error>permission.matches(action, resource) checks if a permission covers the given action and resource. The manage action acts as a wildcard and matches all other actions.
PermissionAction Value Object
Section titled “PermissionAction Value Object”Valid actions: create, read, update, delete, manage.
manageincludes all other actions.action.includes(other)checks containment.action.equals(other)compares values.
ResourceType Value Object
Section titled “ResourceType Value Object”Validates resource names against ^[a-z][a-z0-9]*(-[a-z0-9]+)*$. Examples: posts, user-profiles, audit-logs.
Domain Errors
Section titled “Domain Errors”| Error Class | Condition | Message |
|---|---|---|
InvalidRoleName | Role name is empty | Invalid role name: "<name>". Role name cannot be empty |
DuplicateRole | Role name already exists | Role already exists with name: "<name>" |
RoleNotFound | No role found for the given ID | Role not found with id: "<id>" |
PermissionDenied | Invalid action or resource | Permission denied: invalid action "<value>" |
Domain Events
Section titled “Domain Events”| Event | Emitted By | Payload |
|---|---|---|
RoleAssigned | AssignRole use case | userId, roleId, organizationId?, occurredAt |
RoleRevoked | RevokeRole use case | userId, roleId, occurredAt |
PermissionGranted | CreateRole use case | roleId, permissionId, action, resource, occurredAt |
Application Layer
Section titled “Application Layer”Use Cases
Section titled “Use Cases”CreateRole
Section titled “CreateRole”Creates a new role with optional permissions. Emits PermissionGranted events for each permission.
import { CreateRole } from "./domains/rbac/application/use-cases/create-role.use-case";
const createRole = new CreateRole(roleRepository);
const result = await createRole.execute({ name: "editor", description: "Can edit posts", permissions: [ { action: "read", resource: "posts" }, { action: "update", resource: "posts" }, ],});// Result<{ roleId: string; events: PermissionGranted[] }, Error>Possible failures: DuplicateRole, InvalidRoleName, PermissionDenied
AssignRole
Section titled “AssignRole”Assigns a role to a user.
import { AssignRole } from "./domains/rbac/application/use-cases/assign-role.use-case";
const assignRole = new AssignRole(roleRepository);const result = await assignRole.execute({ userId: "user-1", roleId: "role-1", organizationId: "org-1" });// Result<{ event: RoleAssigned }, Error>Possible failures: RoleNotFound
RevokeRole
Section titled “RevokeRole”Revokes a role from a user. Verifies the user actually has the role before revoking.
import { RevokeRole } from "./domains/rbac/application/use-cases/revoke-role.use-case";
const revokeRole = new RevokeRole(roleRepository);const result = await revokeRole.execute({ userId: "user-1", roleId: "role-1" });// Result<{ event: RoleRevoked }, Error>Possible failures: RoleNotFound
CheckPermission
Section titled “CheckPermission”Checks if a user has a specific permission. Optionally scoped to an organization.
import { CheckPermission } from "./domains/rbac/application/use-cases/check-permission.use-case";
const checkPermission = new CheckPermission(permissionResolver);const result = await checkPermission.execute({ userId: "user-1", action: "update", resource: "posts", organizationId: "org-1", // optional — checks org-scoped permissions});// Result<boolean, PermissionDenied>When organizationId is provided, only permissions assigned within that organization are considered. A user with admin role in org A has no admin privileges in org B.
Possible failures: PermissionDenied (invalid action or resource)
ListRoles
Section titled “ListRoles”Returns all roles in the system.
GetUserPermissions
Section titled “GetUserPermissions”Returns all permissions for a given user.
Port Interfaces
Section titled “Port Interfaces”IRoleRepository
Section titled “IRoleRepository”export interface IRoleRepository { findById(id: string): Promise<Role | null>; findByName(name: string): Promise<Role | null>; findByUserId(userId: string, organizationId?: string): Promise<Role[]>; findAll(): Promise<Role[]>; save(role: Role): Promise<void>; delete(id: string): Promise<void>; assignToUser(userId: string, roleId: string, organizationId?: string): Promise<void>; revokeFromUser(userId: string, roleId: string): Promise<void>;}IPermissionResolver
Section titled “IPermissionResolver”export interface IPermissionResolver { getUserPermissions(userId: string, organizationId?: string): Promise<Permission[]>; hasPermission(userId: string, action: string, resource: string, organizationId?: string): Promise<boolean>;}When organizationId is provided, the resolver filters permissions to those assigned within the specified organization scope. Without it, global (non-org-scoped) permissions are returned.
Public API (contracts/)
Section titled “Public API (contracts/)”import { createAuthorizationService, IAuthorizationService,} from "./domains/rbac/contracts";
const authorizationService: IAuthorizationService = createAuthorizationService({ roleRepository, permissionResolver,});
// IAuthorizationService interface:// createRole(input): Promise<Result<{ roleId: string }, Error>>// assignRole(input): Promise<Result<{ event: RoleAssigned }, Error>>// revokeRole(input): Promise<Result<{ event: RoleRevoked }, Error>>// checkPermission(input): Promise<Result<boolean, PermissionDenied>>// listRoles(): Promise<Result<RoleDTO[], Error>>// getUserPermissions(userId, organizationId?): Promise<Result<PermissionDTO[], Error>>This is the only import consumers need. The internal use case classes are implementation details.
File Map
Section titled “File Map”domains/rbac/ domain/ entities/role.entity.ts entities/permission.entity.ts value-objects/permission-action.vo.ts value-objects/resource-type.vo.ts errors/invalid-role-name.error.ts errors/duplicate-role.error.ts errors/role-not-found.error.ts errors/permission-denied.error.ts events/role-assigned.event.ts events/role-revoked.event.ts events/permission-granted.event.ts application/ use-cases/create-role.use-case.ts use-cases/assign-role.use-case.ts use-cases/revoke-role.use-case.ts use-cases/check-permission.use-case.ts use-cases/list-roles.use-case.ts use-cases/get-user-permissions.use-case.ts ports/role-repository.port.ts ports/permission-resolver.port.ts dto/create-role-input.dto.ts dto/assign-role-input.dto.ts dto/revoke-role-input.dto.ts dto/check-permission-input.dto.ts dto/get-user-permissions-input.dto.ts contracts/ rbac.contract.ts rbac.factory.ts index.ts shared/ result.ts