GovernanceRoles & access

Roles and access

Access is decided in layers rather than by one gate, and the layers answer different questions. Which workspace are you in, are you an admin of it, which records may you see, and may you take this specific action. A role is the input to all four, and it is worth knowing which layer is refusing you.

Two role vocabularies, and they are layered

Every member of a workspace carries one coarse workspace role. That is the value stored on the membership row, the one an admin picks in the team screen, and the one older parts of the product gate on. Six are assignable: super admin, org admin, sales lead, sales rep, support admin and billing admin.

On top of that sits a finer permission matrix of eight internal roles, each holding a named set of grants like leads.view.team or reports.export. Your coarse role is mapped onto one of the eight, and unrecognized values map to the least privileged internal role rather than to nothing, so a mapping gap fails closed.

Super admin, org admin
Both reach every module. Super admin is additionally the only role that can grant super admin, and it cannot be handed out through an email invitation at all.
Sales lead
Dashboard, CRM, communications, insights and support. Maps onto the manager grants: team-wide view and edit, forecast override, sequence management, and the ability to invite.
Sales rep
Dashboard, CRM, communications and support. No insights module. Maps onto the account executive grants: own records, own reports, and running sequences.
Support admin
Dashboard, support and CRM only.
Billing admin
Dashboard, billing and admin only. Holds billing and seat provisioning, and can view the audit log, but holds no grant over leads, deals or sequences.

Approving an outbound send is deliberately withheld from the read-only and billing seats. Every role that runs or manages sequences can approve one; the executive viewer and the billing admin cannot, because a seat that exists to read reports or pay invoices should not be able to release email to a real person.

What actually refuses a request

Four checks sit at different heights, and knowing which one answered tells you what to change.

  1. Workspace resolution. Almost every route resolves the caller's workspace first and refuses with a 403 when there is no membership. This is also what scopes the query: the workspace id is applied in application code on every read and write.
  2. The coarse admin check. A smaller set of routes additionally require an admin workspace role: super admin, org admin, owner or admin. Minting an API key, registering a webhook, and creating, editing or assigning a custom role all sit here.
  3. The permission matrix. A smaller set again asks whether your effective grants include a specific permission, and that is where own-versus-team, exports and the field-level grants are decided.
  4. Row-level security in the database, which enforces the same tenancy boundary for any session-role reader.

Be precise about the third layer: most API routes do not consult the permission matrix at all. It governs the CRM read and write paths, exports and the surfaces built on top of it, not every endpoint in the product. If you are designing around this, gate on the coarse role for coarse things and treat the matrix as what refines access inside the CRM rather than as a universal policy engine.

Own, team, and all

Lead and deal lists resolve a read scope from your grants before the query runs. An all or team grant returns every row in the workspace. An own-only grant narrows the query to rows you are the owner of. No view grant at all resolves to none, which returns nothing rather than everything.

Team collapses to workspace-wide on purpose, and this is the sentence most likely to surprise you. There is no sub-team or reporting-line model in the product, so the workspace is the team boundary: a team grant means every row in the workspace. If you need a manager to see a subset of reps rather than all of them, that structure does not exist yet and no permission expresses it.

Editing follows the same shape and deleting does not. A team-wide edit grant lets you edit any record of that kind; an own-only grant lets you edit records assigned to you, and the refusal says which of the two you hit. Deleting a lead needs its own explicit delete grant. Deleting an opportunity has no permission of its own, so it is held at team-wide deal editing: an own-only seller cannot destroy a deal.

Custom roles

A workspace can compose its own role from the same permission catalog the built-in roles are made of, optionally seeding the checkboxes from one of the eight. Creating, editing, deleting and assigning are admin actions. A member holds at most one custom role, over their existing workspace role.

  • A custom role's permissions are the effective set. It is not unioned with the built-in role it was cloned from, and cloning only seeds the initial selection.
  • Editing a custom role applies immediately to everyone assigned to it. Nothing is snapshotted onto the member, so there is no re-assignment step and no cache to clear.
  • A permission string the catalog does not define is rejected on write rather than stored, so a client cannot invent one.
  • Deleting a custom role does not lock anyone out. The assignment is cleared and those members fall back to their built-in workspace role.
  • The coarse workspace role is never touched by any of this, which is why admin gating and the database policies keep working unchanged.

Field-level grants exist but are deliberately narrow. Three fields can be hidden independently of the broader view and edit grants: compensation, deal margin and contract terms. There is no general field-permission system behind them.

External guests, which are off

There is a least-privilege tier for people outside your organization, and you should read it as not yet available rather than as a feature. It is off in two independent ways, and both would have to be changed deliberately.

  • The tier's grants are behind an environment flag that defaults off. With the flag off the role still resolves, but it holds zero permissions.
  • Separately, a deny-by-default route gate strips the workspace context from a guest on every route that is not on an explicit allowlist, and that allowlist ships empty. The gate applies whether the flag is on or off.

The gate is written that way on purpose: a route added tomorrow refuses a guest without anyone remembering to register it, because the allowlist is the exception rather than the protection. Do not plan an external-collaborator workflow on this tier today.

Behaviour on this page is read from

  • src/lib/admin-access.ts
  • src/lib/rbac-role-library.ts
  • src/lib/custom-roles.ts
  • src/lib/current-user-permissions.ts
  • src/lib/crm-mutation-authorization.ts
  • src/lib/list-read-scope.ts
  • src/lib/external-guest-route-gate.ts
  • src/lib/workspace-invites.ts
  • src/app/api/rbac/custom-roles/route.ts
  • db/migrations/20260702100000_custom_roles.sql

Was this page helpful?

Book a demo