# Project member and invitation contract preview **Development fixture contract.** This standalone [OpenAPI specification](api.openapi.json) and [schemas](schemas.json) let frontend work proceed with generated clients and fixture responses. The same shapes are now present in the [active HTTP contract](../README.md); the isolated preview client and fixture assets remain available for development. The preview supplies contract sources, two isolated Orval outputs and Go API fixture bindings. An explicitly configured development API serves these examples through the normal HTTP/session path. The default API uses the real Project owner reads. The field names are agreed independently of the broader #1066 naming work. 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). The preview retains the active contract's authentication declarations and current owner 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. ## 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 existing Orval configuration generates two preview clients from this same specification. The retained frontend uses plain fetch functions; `services/web` also receives TanStack Query v5 hooks, query keys and options. Both preview clients export: ```ts import { getProjectMembers, listProjectInvitations, } from "@/adaptors/generated/project-member-invitation-preview/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. Outputs are under `services/web/src/adaptors/generated/project-member-invitation-preview/` and `compat/themelio/services/web/src/adaptors/generated/project-member-invitation-preview/`. The canonical web client also exports `useGetProjectMembers` and `useListProjectInvitations`; these need its existing QueryClientProvider. Compat has no TanStack dependency or provider and can keep using fetch functions. Query-cache values must be cleared when the Party session or Project access changes; HTTP `no-store` is not a React Query cache policy. The generated clients make ordinary HTTP requests. Start the Go API with its normal runtime environment loaded, including Valkey, broker and AAI/session configuration, and enable the preview on that API process: ```sh APP_ENV=development \ API_PROJECT_READ_FIXTURES=true \ API_PROJECT_MEMBERS_FIXTURE=members \ API_PROJECT_INVITATIONS_FIXTURE=invitations \ PHAROS_GO_PROJECT=services/api \ python3 scripts/go.py run ./services/api ``` Use the normal frontend origin and route its `/auth` and `/api/v1` requests to this API instance through the existing development proxy. Sign in normally and select a canonical Project returned by `GET /api/v1/me/projects`. Browser requests carry the existing `pharos_session` cookie. Server-rendered reads use the existing server adaptor to forward that cookie. The frontend needs no mock installation, extra request parameters or fixture credential. Fixture mode overlays only these two operations. The API authenticates the current Party and asks the existing Project owner for `project.view` or `project.access.manage` before returning sample data. The broker and existing Project/Party authority must be available; the new member-name and invitation backend queries are unnecessary for this mode. Operator/CLI credentials cannot replace a Party session. Denied or stale authority produces the normal error. `API_PROJECT_READ_FIXTURES` defaults to disabled. Only the literal `true` enables it, and only with `APP_ENV=local` or `development`. Invalid flags/scenarios fail startup. The API logs that Project read fixtures contain synthetic data. Scenario settings are server configuration and require an API restart to change. Member scenarios are `members` (named and unnamed rows with incomplete membership), `members-empty` and `members-name-absent` (a current projection with no name). Invitation scenarios are `invitations` and `invitations-empty`. Either section can independently use status `401`, `404` or `503` to exercise its error state after the real permission check. Omitting a scenario selects `members` or `invitations`. Supplying scenarios while fixture mode is disabled fails startup. The JSON examples remain the source of truth. Generation copies them into the API binary, which validates them against the embedded preview schema during startup. `project_id` is replaced with the requested authorised Project. Invitation `project_revision` comes from the real permission decision; `read_at` is the current fixture response time, and each expiry preserves the example's remaining lifetime. Names, roles, member revisions, counts and invitation contents remain synthetic; they make no claim about that Project's actual contents. The canonical web generation also retains its standard Orval MSW/Faker test outputs. | 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. Keep the two sections' loading/error states independent. An invitation read failure does not invalidate a successful member read. With fixture mode disabled, both operations use the real owner queries and the agreed contract. There is no automatic fixture fallback on an owner failure. Enable the new frontend feature only against the fixture API or a backend deployment that supports the agreed contract. The implementation preserves the existing member query/reply wire shape and adds a distinct owner query for member details, allowing the new owner to run with the old API. The new API rejects an unsupported owner response. Follow the [active contract's deployment order](../README.md#clients-errors-and-rollout), then disable fixture mode and remove the two scenario settings. The frontend keeps the same operation calls, paths and payloads; its preview client continues to work against the implemented API. When consolidating generated outputs, switch the import to `@/adaptors/generated/http/project/project` and remove the preview. Clear cached fixture data or reload the development session during this switch. In 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. ## Regeneration and verification From the repository root, with frozen pnpm dependencies installed: ```sh pnpm generate:contracts ``` Normal contract generation includes the preview: it validates OpenAPI and all five examples, copies the Go API assets and runs the pinned Orval configuration into both preview directories alongside the active HTTP and Protobuf outputs. A contract regression checks equality of the active and preview HTTP shapes. For a focused regeneration, run `uv run --no-project --with openapi-spec-validator==0.7.2 python contracts/project/http_api/member-invitation-preview/generate.py`. Add `--check` to compare all three generated output sets. `pnpm verify` includes this drift check in its contracts group. Exercise the HTTP bindings, owner-decision validation and development composition: ```sh PHAROS_GO_PROJECT=services/api python3 scripts/go.py test \ ./adaptors/http ./adaptors/openapi ./services/api ``` The real Valkey session lane (`pnpm test:party:sessions`) covers fixture HTTP access through the normal session/AUP checks and revocation. Contract examples, HTTP tests and disposable infrastructure qualification remain separate from live member-name/provider behaviour. Once the implementation is deployed and frontend consumers use the active client, remove the preview sources, generation/check hooks, API fixture bindings/configuration and generated outputs.