Exact wire contract for every RBAC screen → endpoint. Base /api/v1; envelope per
00-shared/07 ({success,message,data,meta?,timestamp,requestId} /
{success:false,message,error:{code,details?},timestamp,requestId}). All endpoints from
src/modules/rbac/rbac.controller.ts; rules from rbac.service.ts. Tenant identity
comes from the JWT claim — never from the body.
Aspect Contract
Base https://api.<domain>/api/v1
Headers Authorization: Bearer <accessToken>; x-request-id client UUID; Content-Type: application/json
Tenancy tenantId from JWT (jwt-payload.interface.ts:2); body never carries it (RBAC DTOs have no tenant field)
Rate tier default api 100/min (rate-limit.guard.ts:36-37); production-only enforcement (rate-limit.guard.ts:30)
Server caching permissions-of-user: Redis sl:{tenantId}:perm:{userId} EX 300 (rbac.service.ts:48,68) — client must tolerate ≤ 5 min propagation
Client caching roles/members 5 min stale-while-revalidate; permissions catalog 24 h (09 §2 )
Offline reads from cache + banner; writes blocked
Retry backoff on 5xx/network; no auto-retry on 429
Global order, app.module.ts:129-133:
1. RateLimitGuard (APP_GUARD #1) — sliding window Redis (`rate-limit.guard.ts:20-59`)
2. JwtAuthGuard (APP_GUARD #2) — Bearer verify → req.user {id, tenantId, roles}
+ TenantContext fill, platform_admin flag
(`jwt-auth.guard.ts:40-55`)
3. RbacGuard (APP_GUARD #3) — @Roles (OR, from JWT claims) + @Permissions
(AND, resolved per-user) (`rbac.guard.ts:20-52`)
RbacGuard detail (rbac.guard.ts:20-52):
no @Roles/@Permissions metadata → true (line 29) — route is authed-only.
role match: requiredRoles.some(r => req.user.roles.includes(r)) (lines 38-42) →
else 403 ForbiddenException "Insufficient role."
perm match: getPermissionsForUser(userId, tenantId) (line 44) then
requiredPerms.every(p => perms.includes(p)) (line 48) → else 403 "Insufficient
permission."
Decorators: Roles/ROLES_KEY (roles.decorator.ts:3-4),
Permissions/PERMISSIONS_KEY (permissions.decorator.ts:3-4), re-exported by
guards/decorators.ts:1-5.
Today's coverage (derived, (planned) for completion): only webhooks
(webhooks.controller.ts:22-86), files (files.controller.ts:30-67), scheduler
(scheduler.controller.ts:24-38), search (search.controller.ts:16) controllers
carry @Permissions. Phase-5 "Permissions audit (all endpoints)"
(docs/IMPLEMENTATION_PLAN.md:241) is (planned); the RBAC controller itself is
role-gated (rbac.controller.ts:21). Client must be built to the intended model
(perm-gated everywhere) while living with the current role-gated reality (OQ-R1).
All under class gate @Roles('org_admin') + @UseGuards(JwtAuthGuard, RbacGuard)
(rbac.controller.ts:21-22).
Endpoint GET /rbac/roles (rbac.controller.ts:27-31)
Success 200 data: Role[] — array, no meta (not paginated, rbac.service.ts:75-77)
Sort priority: -1 desc (rbac.service.ts:76)
Role doc shape {_id, tenantId, name, slug, description?, isSystem, priority, permissions[], createdAt, updatedAt, version} (role.schema.ts:67-86, base.schema.ts)
Errors 401 UNAUTHENTICATED; 403 PERMISSION_DENIED (non-org_admin); 429 RATE_LIMITED; 5xx
Endpoint GET /rbac/permissions (rbac.controller.ts:33-37)
Success 200 data: string[] — the 95 names (rbac.service.ts:79-81; permissions.constants.ts:1-97)
Errors same as above
Endpoint POST /rbac/roles (rbac.controller.ts:39-43)
Request CreateRoleDto: {name, slug, description?, isSystem?, priority?, permissions?} (create-role.dto.ts:10-38)
Success 201/200 data: Role (created with tenant from context, role.repository.ts:17-22)
Errors 400 VALIDATION_ERROR (field details); 409 DUPLICATE_RESOURCE — slug exists (rbac.service.ts:86-87, index role.schema.ts:89); 403; 429
Client note always send isSystem:false (OQ-R2); whitelist permissions client-side
Endpoint PATCH /rbac/roles/:id (rbac.controller.ts:45-49)
Request same CreateRoleDto (full-doc $set, rbac.service.ts:96)
Errors 400 "Cannot modify system roles." (rbac.service.ts:94-95); 404 RESOURCE_NOT_FOUND (rbac.service.ts:93,97); 409 on slug clash; 403
Success 200 data: Role (updated doc)
Endpoint DELETE /rbac/roles/:id (rbac.controller.ts:51-55)
Behaviour soft delete (rbac.service.ts:106); 400 system role (rbac.service.ts:104-105); 404 unknown (rbac.service.ts:103)
Success 200 data: <void> (empty)
Endpoint GET /rbac/members (rbac.controller.ts:57-61)
Success 200 data: Member[] — array, no meta (rbac.service.ts:109-111)
Sort joinedAt: -1 desc (rbac.service.ts:110)
Member doc shape {_id, tenantId, userId, organizationId?, roles[], permissions[], status, joinedAt, invitedBy?, acceptedAt?, lastActiveAt?, metadata?, createdAt, updatedAt, version} (organization-member.schema.ts:14-44)
Note no user-profile join (OQ-R7); permissions[] field exists but is never written by the service (derived perms only, rbac.service.ts:63-65)
Endpoint POST /rbac/members (rbac.controller.ts:63-67)
Request AddMemberDto: {userId (MongoId), roles: string[]} (add-member.dto.ts:4-12)
Behaviour creates status:"active", joinedAt: now, tenant-scoped (rbac.service.ts:113-127)
Errors 400 validation; 403; 500 on duplicate (E11000 on {tenantId,userId} index organization-member.schema.ts:48 — unmapped, OQ-R5) → client pre-checks
Success 200/201 data: Member
Endpoint PATCH /rbac/members/:id (rbac.controller.ts:69-73)
Request AddMemberDto — userId ignored by service (rbac.controller.ts:72; rbac.service.ts:129-136 $set:{roles})
Errors 404 RESOURCE_NOT_FOUND (rbac.service.ts:134); 400; 403
Success 200 data: Member
Endpoint DELETE /rbac/members/:id (rbac.controller.ts:75-79)
Behaviour soft delete; silent 200 even if not found (rbac.service.ts:138-140)
Success 200 data: <void>
Endpoint GET /audit-logs?page=&limit=&action=&entityType=&actorId= (audit.controller.ts:17-34)
Guard JwtAuthGuard only today (audit.controller.ts:9) — audit.read not enforced (Phase-5 (planned); client gates anyway)
Success 200 paginated: data: AuditLog[] + meta: {page, limit, totalItems, totalPages, hasNext, hasPrevious} (07 §2; default limit 50, audit.controller.ts:20)
Filter action / entityType / actorId combined (audit.controller.ts:26-29)
Screen code UI
any list 401 silent refresh → fail → sessionExpired
any 403 PERMISSION_DENIED 403 screen w/ cause copy; hide offending action
role save 409 DUPLICATE_RESOURCE inline under slug + focus
role save/delete 400 (system role) unreachable via UI; banner fallback
role update/delete 404 treat as removed → pop to list + snackbar
member add 500 (dup, E11000) pre-checked; fallback conflict banner + list refresh
member update 404 row removed snackbar
any write 429 countdown banner, no auto-retry
any 5xx generic + requestId (http-exception.filter.ts:60-65)
Controller group Status today Source
webhooks (11) @Permissions('webhook.*') on allwebhooks.controller.ts:22-86
files (5) @Permissions('file.*') on allfiles.controller.ts:30-67
scheduler (3) @Permissions('scheduler.*') on allscheduler.controller.ts:24-38
search (1) @Permissions('search')search.controller.ts:16
rbac (8) @Roles('org_admin') class-levelrbac.controller.ts:21
all other modules global guards only (authed) app.module.ts:129-133
full permission audit (planned) Phase 5docs/IMPLEMENTATION_PLAN.md:241