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_role | org_memberships.org_role | |
|---|---|---|
| Question | "who is this account across the platform?" | "what can they do inside this org?" |
| Values | superadmin, account_manager, expert, ai_operator, org_admin, org_member, client_admin, client_member | owner, admin, member, billing |
| Default | org_member | — (set per membership) |
| Scope | global (one per user) | per (user, org) membership |
| Guard | PlatformRolesGuard (@PlatformRoles(...)) | OrgRolesGuard (@OrgRoles(...)) |
| Defined | auth.entities.ts:16 | auth.entities.ts:53 |
- Staff users (
superadmin/account_manager/expert/ai_operator) useplatform_roleonly — they have noorg_role. - Workspace users use the client-facing
platform_rolevalues (org_admin|org_member) and anorg_role. For per-workspace permissions,org_roleis authoritative (enforced byOrgRolesGuard).
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:
| Role | Multiplicity | Can | Cannot |
|---|---|---|---|
owner | exactly 1 per org | everything: transfer ownership, delete workspace, manage billing/members/settings | — |
admin | multiple | manage members, settings, integrations, channels, billing | delete workspace, transfer ownership |
billing | multiple | view billing & usage | access workspace data |
member | multiple | view & use conversations | manage 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+admincannot leave (organizations.service.ts:373-382). There is no equivalent forbilling. owneris the onlyorg_rolewith enforced cardinality;admin/member/billingmay 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 event | platform_role | org_role | synced? | source |
|---|---|---|---|---|
| Self-serve register | org_admin | admin | ✅ | auth.service.ts:153,162 |
| AM invite (admin) | org_admin | admin | ✅ | invite carries both; consumed in auth.service.ts acceptInvite (~:1025,1051) |
| AM invite (member, default) | org_member | member | ✅ | same path, defaults |
| Onboarding complete (creator) | org_admin | owner | ✅ | onboarding.service.ts:236,322 (and idempotent :700,713) |
| Onboarding-invited member | org_member | member | ✅ | onboarding.service.ts:517,530 |
| Member accepts invite | from invite | from invite | ✅ | auth.service.ts acceptInvite |
| Promote member → admin | unchanged | admin | ❌ | admin.service.ts:606 (sets org_role only) |
| Transfer ownership | unchanged | owner / admin | ❌ | organizations.service.ts:344,346 (sets org_role only) |
Consequence: a user can legitimately be
platform_role=org_memberandorg_role=admin(or evenowner) after a promotion/transfer. When reasoning about per-workspace permissions, readorg_role, notplatform_role.
Client org_role → platform_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_role | platform_role |
|---|---|
owner | org_admin |
admin | org_admin |
member | org_member |
billing | org_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_rolein the Team section (options includeowner), but the platform-role label you may see elsewhere isplatform_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 noowner(see next point). - An org may have no
owner. Only the onboarding-completer path writesorg_role=owner. Orgs created via/auth/registeror AM-invite without completing the onboarding wizard have anadminbut noowner(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 useorg_admin/org_member+org_role.
See also
api/src/auth/WORKSPACE-ROLES.md— workspace-role technical reference.GLOSSARY.md— role definitions (Client Roles section).docs/design/VIEWS_BY_ROLE.md— per-view permission matrix byorg_role.