Team
Manage the people in your Pact workspace — invite users, assign roles, elevate access just-in-time, and disable members — all tenant-scoped and audited.
Team
The Team surface is where a workspace owner or admin manages the people who
can sign into Pact. Every member belongs to exactly one tenant, carries a role
that drives their permissions, and has a lifecycle you control end to end:
invite, accept, elevate, and disable. All of it is backed by the /v1/team
API and enforced server-side.
Live
Members, invites, role changes, just-in-time elevation, and member disablement
are fully implemented in api/routes/team.py and gated by verify_api_key +
the admin module. Every mutation emits a domain event so the timeline is
honest about who changed what.
Members
A member is a row in tenant_users — email, name, role, status,
last_login_at, manager_id, and the invite that created them
(invited_by_user_id, accepted_invite_id).
GET /v1/team/members— list everyone in the workspace. Each entry carries the storedroleplus aneffective_role: if the user currently holds a just-in-time elevation,effective_role(and ajit_untilexpiry) reflect it while the stored role stays unchanged.GET /v1/team/members/{user_id}— a single member with their recent role-change history drawn from the event stream.
Reading the roster requires the team:read permission (held by manager and
above); it is not something a plain member or viewer can enumerate.
Inviting people
Invites are bulk-friendly.
- 1
Create invites
POST /v1/team/inviteswith a list ofemailsand a targetrole(defaults tomember). Requiresteam:invite. Each invite gets a signed accept link; delivery status is reported per address. - 2
Invitee accepts
The recipient opens the link and completes acceptance at the public
/v1/authaccept-invite endpoint — no API key required, because they don't have one yet. Acceptance creates theirtenant_usersrow. - 3
Manage pending invites
GET /v1/team/inviteslists outstanding invites;POST .../resendre-sends the email;DELETE .../{id}revokes one that shouldn't be honored.
Changing a member's role
POST /v1/team/members/{user_id}/role sets a member's stored role. The body is
validated against the canonical role set (owner, admin, manager,
member, viewer, api) and the change requires team:manage_roles. The old
and new role are written to the audit timeline.
Just-in-time elevation
Sometimes a member needs elevated access for a bounded window — to run a
migration, or cover an on-call gap — without permanently promoting them.
POST /v1/team/members/{user_id}/elevate grants a temporary role with a hard
expiry (stored in time_bounded_roles).
How expiry stays honest without a cron
Expired elevations are reverted lazily: any read of a member's effective role
stamps overdue rows with reverted_at and appends a
team.role.elevation_reverted event the first time it observes the expiry. The
role is always truthful at read time — no background worker required.
Disabling a member
POST /v1/team/members/{user_id}/disable flips a member's status so they can
no longer authenticate, without deleting their history. This is the reversible,
audit-friendly alternative to hard removal.
Tenant isolation
Every endpoint derives tenant_id from the authenticated caller — never from
the request body — and every query is scoped to that tenant. A user in one
workspace cannot read, elevate, or disable a member of another.