Skip to main content

Roles & Permissions

Authoritative reference for the roles a workspace (client/org) user carries, how the two role systems relate, and which one governs what.

Source of truth: PlatformRole / OrgRole types in api/src/auth/auth.entities.ts; guards in api/src/auth/roles.guard.ts.

The two role systems

Every authenticated principal has a platform role. A user who belongs to a workspace also has a workspace role per org they're a member of. They are separate columns and answer different questions.

users.platform_roleorg_memberships.org_role
Question"who is this account across the platform?""what can they do inside this org?"
Valuessuperadmin, account_manager, expert, ai_operator, org_admin, org_member, client_admin, client_memberowner, admin, member, billing
Defaultorg_member— (set per membership)
Scopeglobal (one per user)per (user, org) membership
GuardPlatformRolesGuard (@PlatformRoles(...))OrgRolesGuard (@OrgRoles(...))
Definedauth.entities.ts:16auth.entities.ts:53
  • Staff users (superadmin / account_manager / expert / ai_operator) use platform_role only — they have no org_role.
  • Workspace users use the client-facing platform_role values (org_admin | org_member) and an org_role. For per-workspace permissions, org_role is authoritative (enforced by OrgRolesGuard).

Workspace roles (org_role) — owner / admin / member / billing

See api/src/auth/WORKSPACE-ROLES.md and docs/design/VIEWS_BY_ROLE.md for the full UI-permission matrix. Summary:

RoleMultiplicityCanCannot
ownerexactly 1 per orgeverything: transfer ownership, delete workspace, manage billing/members/settings
adminmultiplemanage members, settings, integrations, channels, billingdelete workspace, transfer ownership
billingmultipleview billing & usageaccess workspace data
membermultipleview & use conversationsmanage team/settings/billing

owner-only capabilities (admin does not have these):

  • Transfer ownership — organizations.controller.ts:962 (@OrgRoles("owner")).
  • Cannot leave the org — organizations.service.ts:362-365.
  • Role is immutable except via transfer — admin.service.ts:603-605.
  • Cannot be removed — admin.service.ts:617-621.

Guards:

  • There is a "last admin" guard — the only/last owner+admin cannot leave (organizations.service.ts:373-382). There is no equivalent for billing.
  • owner is the only org_role with enforced cardinality; admin/member/ billing may be held by many members.

platform_role ↔ org_role mapping & lifecycle

For a workspace user the two are synced at creation (register / invite / onboarding), but only org_role changes on promotion and ownership transfer — so they intentionally desync afterwards.

Lifecycle eventplatform_roleorg_rolesynced?source
Self-serve registerorg_adminadminauth.service.ts:153,162
AM invite (admin)org_adminadmininvite carries both; consumed in auth.service.ts acceptInvite (~:1025,1051)
AM invite (member, default)org_membermembersame path, defaults
Onboarding complete (creator)org_adminowneronboarding.service.ts:236,322 (and idempotent :700,713)
Onboarding-invited memberorg_membermemberonboarding.service.ts:517,530
Member accepts invitefrom invitefrom inviteauth.service.ts acceptInvite
Promote member → adminunchangedadminadmin.service.ts:606 (sets org_role only)
Transfer ownershipunchangedowner / adminorganizations.service.ts:344,346 (sets org_role only)

Consequence: a user can legitimately be platform_role=org_member and org_role=admin (or even owner) after a promotion/transfer. When reasoning about per-workspace permissions, read org_role, not platform_role.

Client org_roleplatform_role mapping

For a client user, platform_role is a coarse admin-vs-member flag that tracks org_role: admin-level roles map to org_admin, member-level roles map to org_member. (platform_role also marks "this is a client account" for post-login routing to /client/* and the internal-vs-client host boundary.)

org_roleplatform_role
ownerorg_admin
adminorg_admin
memberorg_member
billingorg_member

This is what the creation paths assign (auth.service.ts:153, onboarding.service.ts:236). Caveat — they can desync: a later promotion or ownership transfer changes only org_role (see the lifecycle table above), so an existing user may end up org_role=admin with a stale platform_role=org_member (or vice-versa). For per-workspace authorization, org_role is authoritative — read it, not platform_role.

Gotchas

  • SuperAdmin "org admin" ≠ owner. The SuperAdmin/AM client views render org_role in the Team section (options include owner), but the platform-role label you may see elsewhere is platform_role (org_admin / org_member), which never says "owner". If a client list shows only "org admin", that's the platform role, or the org genuinely has no owner (see next point).
  • An org may have no owner. Only the onboarding-completer path writes org_role=owner. Orgs created via /auth/register or AM-invite without completing the onboarding wizard have an admin but no owner (auth.service.ts:162).
  • client_admin / client_member (auth.entities.ts:23-24) are never assigned to any user in the codebase. They appear only in a few defensive authorization allow-lists (e.g. video-calls.controller.ts, conversations.service.ts, kb.service.ts); in practice no user holds them. New code should use org_admin / org_member + org_role.

See also