# Project members and invitations The default Go API serves these reads through the Project owner. The [development fixture mode](member-invitation-preview/README.md#clients-errors-and-rollout) remains available through explicit configuration. Generated clients and disposable fixture checks do not establish support in a deployed backend. Both reads require a current Party identity and a canonical Project UUID. Browsers use the Party session; the enrolled MCP gateway can use its [operation-bound assertion](../../mcp/README.md#authority) under the same current owner permission checks. Responses use `Cache-Control: private, no-store`. Neither operation accepts query parameters. These reads add no mutation, realtime subscription or automatic polling. ## Members `GET /api/v1/projects/{projectId}/members`, operation `getProjectMembers`, requires current `project.view` authority. The backend checks Project/source authority before and after the read. ```json { "project_id": "11111111-1111-4111-8111-111111111111", "revision": 12, "items": [ { "party_id": "22222222-2222-4222-8222-222222222222", "roles": ["project-administrator"], "asserted_name": "Alex Morgan", "asserted_name_revision": 7 }, { "party_id": "33333333-3333-4333-8333-333333333333", "roles": ["developer"], "asserted_name": null, "asserted_name_revision": null } ], "pending_member_count": 1, "membership_complete": false } ``` All properties are required. `asserted_name` and `asserted_name_revision` are nullable. The name is the existing trusted AAI identity projection's name; editable profile names and email addresses are never used as fallbacks. A current projection with no name has a null name and its non-null revision. A pending, superseded or currently ineligible identity projection has both fields null. A missing name preserves the membership row and roles. Infrastructure or corrupt-authority failures fail the whole query. `revision` versions Project membership. `asserted_name_revision` independently versions the Party identity projection and must not be compared between Parties. An unchanged Project revision does not imply unchanged names. `items` is ordered by `party_id`. The complete source observation contains at most 256 member bindings; several qualified bindings can contribute to one Party row. Every observed binding is resolved into a row or counted in `pending_member_count`. `membership_complete` equals `pending_member_count === 0`. This flag concerns membership resolution; it says nothing about missing names. The count is diagnostic information and must never be labelled as invitations. Show available rows and a simple incomplete-membership state when the flag is false. Use a neutral missing-name label or shortened canonical Party ID. The retained Project page uses `asserted_name` for every member, including the viewer. Public profile visibility does not affect this authorised list. Its own-profile read identifies the `You` marker only. The browser regression runs with private/absent public profiles, independent name revisions, unavailable reads and a delayed response from a previously selected Project: ```sh pnpm exec playwright test --config acceptance/browser/project-members.config.ts ``` ## Invitations `GET /api/v1/projects/{projectId}/invitations`, operation `listProjectInvitations`, requires current `project.access.manage` authority, currently granted to Project administrators. Show this separate section only to those administrators. ```json { "project_id": "11111111-1111-4111-8111-111111111111", "project_revision": 12, "read_at": "2026-11-06T12:00:00Z", "items": [ { "invitation_id": "44444444-4444-4444-8444-444444444444", "recipient_email": "new.member@example.invalid", "roles": ["developer"], "state": "pending", "expires_at": "2026-11-20T12:00:00Z" } ] } ``` All properties are required and non-null. `invitation_id` is an opaque reference, not a canonical Party ID or acceptance credential. The backend derives this scoped reference without exposing the provider invitation locator. `recipient_email` is available only through this administrator-authorised read and is not an asserted name. Roles use the same canonical values as members. This slice returns only provider-pending invitations whose expiry is after `read_at`. It excludes accepted, cancelled, rejected and expired invitations. There is no historical invitation list or acceptance/revocation operation. `expires_at` is the provider's actual expiry, not a locally invented lifetime. The backend resolves the exact canonical Project/provider binding, traverses all provider pages and checks Project scope and customer itself. It rechecks current administrator authority after acquisition. `items` contains the complete result, bounded to 256 invitations and sorted by `expires_at`, then `invitation_id`. There is no cursor. Overflow, missing pages, unsupported roles and malformed provider data fail the whole request; partial results are never returned. `project_revision` versions the Project authority used for this read, not the invitation collection. `read_at` is successful acquisition completion time. The provider supplies no atomic collection revision; this timestamp does not establish a transactional snapshot across provider pages. ## Clients, errors and rollout The source schemas are [members.paths.json](members.paths.json), [invitations.paths.json](invitations.paths.json) and [schemas.json](schemas.json). Regenerate with `pnpm generate:contracts`. Both generated clients export: ```ts import { getProjectMembers, listProjectInvitations, } from "@/adaptors/generated/http/project/project"; const members = (await getProjectMembers(projectId)).data; const invitations = (await listProjectInvitations(projectId)).data; ``` The import is relative to the active frontend's `src` alias. Sources are `services/web/src/adaptors/generated/http/project/project.ts` and `compat/themelio/services/web/src/adaptors/generated/http/project/project.ts`. The canonical web client also exports `useGetProjectMembers` and `useListProjectInvitations`. Query-cache values must be cleared when the Party session or Project access changes; HTTP `no-store` is not a React Query cache policy. | Status | Meaning | Frontend handling | | --- | --- | --- | | `200` | Successful bounded read | Empty invitations prove an empty invitation list; an empty roster proves emptiness only when `membership_complete` is true. | | `400` | Invalid Project ID or any query parameters | Fix the request. | | `401` | Missing or no longer current Party session | Follow the existing sign-in/session flow. | | `404` | Missing or inaccessible Project for this operation | Show unavailable/access state; do not reveal whether the Project exists. | | `503` | Stale/changing authority, unavailable dependency, incomplete/oversized acquisition or invalid response | Show unavailable/retry state, retaining no claim that the list is empty. | The generated fetch adaptor rejects non-success responses with the existing `HTTPError`; inspect its status. Error bodies contain `code`: `invalid_input` for 400, `unauthorised` for 401, `not_found` for 404 and `temporarily_unavailable` for 503. Keep the two sections' loading/error states independent. An invitation read failure does not invalidate a successful member read. Reconcile the API-auth credentials and deploy the Waldur actor capability before the new Project owner, then deploy the new API and enable frontend use of the new fields. The [invitation adaptor guide](../../../services/beto/contexts/project/adaptors/waldur/README.md#project-invitation-read) describes the scoped credential, URL and trust requirements. The established owner member query retains its original wire shape for existing API instances. The new API calls `GetProjectMemberDetails`; an older owner cannot satisfy that query and the read fails unavailable. Its reply preserves presence of `membership_complete`, and the API rejects absent presence. For real data, leave `API_PROJECT_READ_FIXTURES` unset or `false` and remove both fixture scenario settings. An unavailable owner or provider produces an error; the API never substitutes fixture data. Clear any cached fixture responses when switching the development frontend to real reads. Deploy backend support before relying on the new required fields. During a mixed-version rollout, absent fields are unsupported-contract evidence, not equivalent to null, `false` or zero. An old backend's invitation-route `404` must not be converted into an empty invitation list. Frontend fixture work can proceed against [fixtures/members.json](fixtures/members.json), [fixtures/invitations.json](fixtures/invitations.json) and [fixtures/invitations-empty.json](fixtures/invitations-empty.json); these are contract examples, not live deployment evidence.