Skip to content

RBAC & permissions ​

Access control lives in packages/permissions: pure resolution logic behind store ports, with an Arkormˣ-backed implementation in the API. Permissions are flat strings grouped by domain; roles bundle permissions; users get the union of their role permissions plus any direct grants.

Permission catalogue ​

Permissions are group.action strings. The full catalogue:

GroupPermissions
organizationsorganizations.view, organizations.update, organizations.delete
membersmembers.view, members.invite, members.update, members.remove
projectsprojects.view, projects.create, projects.update, projects.delete
api_keysapi_keys.view, api_keys.create, api_keys.revoke
webhookswebhooks.view, webhooks.create, webhooks.update, webhooks.delete, webhooks.test
sessionssessions.view, sessions.create, sessions.cancel, sessions.retry, sessions.export
reviewsreviews.view, reviews.assign, reviews.approve, reviews.reject, reviews.request_retry, reviews.note
audit_logsaudit_logs.view
settingssettings.view, settings.update
billingbilling.view, billing.update

PermissionSync.permissions(store) upserts the whole catalogue idempotently (run on seed / boot).

Default system roles ​

Five system roles are created per organization by PermissionSync.roles(organizationId, store):

RoleSlugPermissions
OwnerownerThe entire catalogue.
AdminadminEverything except organizations.delete and billing.update.
Reviewerreviewersessions.view + all reviews.* (view/assign/approve/reject/request_retry/note).
DeveloperdeveloperIntegration: projects.view, all api_keys.*, all webhooks.*, sessions.view, sessions.create, audit_logs.view, settings.view.
Read-onlyreadonlyEvery *.view permission across the catalogue.

System roles are marked is_system and are read-only in the dashboard. Organizations can also create custom roles with any permission subset.

How effective permissions resolve ​

Permissions.resolve({ userId, organizationId, projectId? }, store) returns the deduplicated union of four sources:

  1. Organization role permissions (the user's OrganizationMember.role).
  2. Project role permissions (the user's ProjectMember.role, when projectId is given).
  3. Direct organization grants: user_permissions rows with project_id IS NULL.
  4. Direct project grants: user_permissions rows with project_id = projectId.
effective = dedupe(organizationRole ∪ projectRole ∪ directOrganization ∪ directProject)

There is no precedence ordering and no negative permissions; direct grants only add to role permissions; they can't revoke. Resolution is pure; the API provides an ArkormPermissionStore that eager-loads the joins (no N+1).

Enforcement ​

  • hasPermission(perms, 'sessions.view') (+ hasAny / hasAll / ensurePermission) for in-code checks.
  • authorize(ctx, permission, store) throws PermissionDeniedError (403).
  • In the API, the can('sessions.view') middleware factory gates routes; it runs after auth + resolveOrganization. The required permission for each route is listed in the Dashboard API reference.

Released under the MIT License.