Team roles
Pact's canonical roles, the permission keys they grant, per-tenant overrides, custom roles, and just-in-time elevation — the full RBAC model.
Team roles
Every action in Pact is gated by a permission, and every member holds a role that resolves to a set of permissions. The role model is deliberately small and legible: a fixed set of canonical roles, a per-tenant override layer for the exceptions, and optional custom roles cloned from a template.
Live
Role resolution, per-tenant overrides, custom roles, and just-in-time elevation
are enforced in core/role_permissions.py and core/permissions.py, and
exposed through /v1/team/roles. Route handlers gate on these permissions via
core.permissions.require_permission / requires_role.
Canonical roles
The built-in roles, from most to least privileged, defined in
CANONICAL_GRANTS:
| Role | What it can do |
|---|---|
owner | Everything, including billing:manage. |
admin | Everything except billing:manage. |
manager | Member permissions plus team:read and team:invite. |
member | Read + write across CRM objects, sequences (incl. send), reports, consent, enrichment. |
viewer | Read-only across every surface. |
support | Member-tier CRM plus team:read (to pick a user to impersonate) — no admin, billing, or invite. |
compliance | Read-only (DPO): includes consent:read and audit_log:read. |
api | Member-tier, tuned for programmatic key-based callers. |
Privacy verbs live in a second vocabulary
The compliance (DPO) role is read-only in the coarse override vocabulary
above. The fine-grained privacy verbs a DPO also needs — DSAR export, voice
vault reveal — are declared in the typed core.permissions.Permission enum,
not in these override keys. A tenant that wants its DPO to execute erasure
grants consent:write via the override UI; it is off by default.
Permission keys
Permissions are string keys grouped by surface. The canonical set
(PERMISSION_KEYS) includes:
accounts:read|write|delete contacts:read|write|delete
opportunities:read|write|delete sequences:read|write|send
billing:read|manage team:read|invite|manage_roles
audit_log:read reports:read|write|share
webhooks:manage api_keys:manage
integrations:connect consent:read|write
enrichment:run
Check whether the current caller holds a specific key with
GET /v1/team/permissions/check?key=billing:manage — it returns
granted, a human reason, and the caller's resolved role.
Per-tenant overrides
Defaults aren't always right for every workspace. A row in the
role_permissions table flips a single permission for a single role inside one
tenant: granted=true adds it, granted=false removes it. This is how an admin
can grant billing:manage to the admin role (denied by default) without
shipping a code change.
GET /v1/team/roles returns, for each role, its permissions (effective),
canonical_permissions (the code default), and the deltas
added_by_tenant / removed_by_tenant so the override is always visible.
Toggle overrides with PATCH /v1/team/roles/{role_name}/permissions
(requires team:manage_roles).
Custom roles
Need a role the canonical set doesn't cover? POST /v1/team/roles clones a
canonical role as a template into a new named role (the name must match
^[a-z][a-z0-9_]*$), materializing its grants into role_permissions. From
there you tune it with the same PATCH toggle endpoint. Custom roles show up in
GET /v1/team/roles with kind: "custom".
Just-in-time elevation
Temporary elevation lets a member borrow a higher role for a bounded window.
Grants live in time_bounded_roles with a hard granted_until. The resolver
(observe_jit) treats an active grant as the member's effective_role for
permission checks, and reverts expired grants lazily on the next read — emitting
a team.role.elevation_reverted audit event exactly once.
Owner is always admitted
Role-shaped gates (requires_role("admin")) always admit owner, because the
owner holds the full permission set — a gate that named admin but rejected
owner would be a bug, not a feature.