PPactDocs
Team & Org

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:

RoleWhat it can do
ownerEverything, including billing:manage.
adminEverything except billing:manage.
managerMember permissions plus team:read and team:invite.
memberRead + write across CRM objects, sequences (incl. send), reports, consent, enrichment.
viewerRead-only across every surface.
supportMember-tier CRM plus team:read (to pick a user to impersonate) — no admin, billing, or invite.
complianceRead-only (DPO): includes consent:read and audit_log:read.
apiMember-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:

code
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.