PPactDocs
Team & Org

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_usersemail, 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 stored role plus an effective_role: if the user currently holds a just-in-time elevation, effective_role (and a jit_until expiry) 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. 1

    Create invites

    POST /v1/team/invites with a list of emails and a target role (defaults to member). Requires team:invite. Each invite gets a signed accept link; delivery status is reported per address.

  2. 2

    Invitee accepts

    The recipient opens the link and completes acceptance at the public /v1/auth accept-invite endpoint — no API key required, because they don't have one yet. Acceptance creates their tenant_users row.

  3. 3

    Manage pending invites

    GET /v1/team/invites lists outstanding invites; POST .../resend re-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.