Roles, teams, and permissions
Every request you make to Orbit resolves an identity, and every member of your organization carries a role. Between the two the platform decides what that member may do. This page defines the role vocabulary, the permission model that gates routes on it, and the Teams feature that groups members for routing - so when a member is denied with403 you know which layer refused them, and when you design access you put a person on the right seat.
This page governs within-org access control. For which organization a request resolves to, see The subaccount organization model. For the credentials a member presents, see Authentication and session model.
Section 1 - The built-in role vocabulary
Ten named roles exist. Every member of your org is assigned exactly one, and that assignment is the anchor for what the route gates allow.
Three design points become intuitive once you see them:
- The hierarchy is a floor, and sub-viewer roles sit deliberately below the viewer. A route that names one role in
requireRole(...)admits every role ranked at or above that role’s level. Becausesupervisor,marketer,analyst,agent, anduserrank belowviewer, none of them inherits the viewer-tier reads; each satisfies ONLY the routes that explicitly list it. billingis orthogonal. Its own route set either namesbillingexplicitly or it does not admit it by hierarchy at all -requireRoletreats the role as a special case, so a disconnect between “money” and “management” is preserved on both directions.- The least-privilege seats were added for operator tiers, and none of them can alter outbound voice/SMS routing. Outbound termination configuration stays on
owner/admin/developeralone - the operator seats reach reading and console actions, never provider or trunk configuration.
Section 2 - How gates work: requireRole, requireGranularPermission
Two gate families sit between the route’s route handler and its logic; both read the resolved member’s role from the request context the auth middleware populated. Both return the same 401/403 envelope.
requireRole(...) - floor check. The route declares the roles it allows. The gate computes the user’s role level and admits them if that level is at or above the level of any allowed role (billing handled as the special case described in Section 1). Most Orbit routes gate this way.
requireGranularPermission("<key>") - capability check, custom-role aware. The route declares a single whitelisted permission key (a resource:action string such as team:invite) and the evaluator decides:
- If the member is assigned a custom role (see Section 4), evaluation is strict and deny-by-default: the request passes iff the role’s granted set contains the key. The custom role replaces the member’s underlying fixed role for this check - it does not union with it.
- If the member is on a built-in role, evaluation consults a fixed grant table that mirrors the platform’s canonical permission table for every key that overlaps it, and adds explicit least-privilege grants for new keys. Behaviour for every built-in-role member is identical to what the corresponding built-in role always allowed.
requireGranularPermission("team:invite") onto the owner/admin routes was behaviour-preserving for built-in members; it only added the custom-role-aware branch. A super-admin (break-glass / platform staff) always passes either gate, and the 401/403 envelope is identical to every other gate.
Section 3 - Teams: the endpoint vocabulary
A team is a named group of org members. The routing rule actionassign_team targets a team, so inbound messages spread across a group rather than defaulting to whoever is unlucky enough to be unassigned at the moment.
The endpoints are:
Writes all pass through
team:invite, so on built-in roles that is owner/admin. Where the custom role is in play, the member’s custom grant must include team:invite. Reads are any-member, so the inbox “assign to team” picker works for your agents too.
A routing rule that picks assign_team resolves a target team, finds its active members, and picks the one with the fewest currently-open assigned conversations - a round-robin/fairness ordering by (open count, last_assigned_at, user_id). That picked member becomes assigned_to, and the team’s last_assigned_at bumps as a fairness aid. When the team has no active members the action refuses to mis-assign: it posts an internal note and leaves the conversation unassigned.
Section 4 - Custom roles: deny-by-default grants
A custom role is a tenant-named role you define in the dashboard: you give it a name (for example, “Support read-only”) and a set of whitelisted permission keys it grants. Assign a member to it and the granular gates evaluate them strictly against that set - replacing the member’s built-in role, not unioning with it. Ten high-sensitivity capability keys are whitelisted today, and the set grows only as more sensitive abilities move into it:
Two guardrails keep custom roles safe:
- The route layer refuses to put the org owner on a custom role. The owner must never be locked out of recovery actions, so custom roles are a member-level tool, and assigning one to an owner is rejected.
- A persisted role can never grant a capability outside the whitelist. The
permissionspayload you save is re-filtered to the whitelist at write time, so a typo or a rogue payload hash stays inert.
Section 5 - Where roles meet the subaccount model
Roles are a user-level tool within one organization; subaccounts are an org-scope tool within one tenant. The two intersect at the org-resolving credential and stay orthogonal from there.- The user’s role is scoped to the org their request resolves to. A member admin of one subaccount holds that role only on requests its API key resolves to that subaccount; they do not become admin of the parent.
- Subaccounts share one tenant schema, so org ownership gates reads/writes; roles gate capabilities. No role widens a caller beyond their org, and the org-ownership gate never admits another org’s row. The two gates stack; they never compose into a wider one.
- Teams are org-scoped and route-scoped. A team belongs to the org that created it (the table is query-plan scoped to the org’s tenant schema), and the
assign_teamrouting rule that references it belongs to the same org’s routing rules.
Section 6 - Where this model hands off to other pages
This model is deliberately narrow - capabilities within one org. Three sibling pages own the parts this one references:- The subaccount organization model - the org tree, per-org attribution, and cross-subaccount visibility. Subaccounts are soft scope; roles are hard (within-org) scope.
- Authentication and session model - which credential class a request carries and how the server resolves it. Roles matter only after a credential has resolved a member.
- Tenant isolation - the hard boundary above all of this. Custom roles and teams live inside one tenant’s schema; they never cross it.
Cross-references
- The subaccount organization model - the org-scope sibling.
- Authentication and session model - the credential-resolution sibling.
- Tenant isolation - the hard boundary underneath.