---
id: w6/m10/t006
title: "Render parity: sweep REST/GraphQL/MCP/UI for identical fields + semantics vs Render's team surface"
worker: worker6
status: done
estimate: 30m
depends_on: [w6/m10/t005]
---

## Objective

Standing closing task. Verify the milestone's change is consistent across every surface it exposes — REST, GraphQL, MCP (same fields, semantics, error shapes; `docs/ADR006-bex-api.md`) and the dashboard Team page — and compare against Render's team-members behavior (the t001 evidence). Flag any drift as follow-up work (inbox note) rather than silently diverging.

## Context

- Surfaces: `lego/backend/internal/members/{rest,graphql,mcp}.go`, `dashboard/src/features/team/`.
- The owners API's `renderTeamMember` (`lego/backend/internal/workspaces/render.go`) is the in-repo Render-shape reference — the two member surfaces should agree on `userId`/`email` values for the same member.
- t001's decision record holds the verified Render shape + the mutation-keying verdict.

## Steps

1. List one member set through all three backend surfaces + the owners API; diff field names/values.
2. Check the Team page against Render's Team UI treatment (t001 capture).
3. File an inbox note for any accepted drift (e.g. mutation keying).

## Files

- `lego/backend/internal/members/` (verification; fixes only if trivially in-scope)
- `.pm/w6/` (follow-up note if drift is flagged)

## Acceptance criteria

- [x] Same member ⇒ identical `userId`/`email` across members REST/GraphQL/MCP and the owners API.
- [x] Every accepted divergence is written down (ADR018 row or a filed follow-up), none silent.

## Verification (2026-07-12)

**Three-surface parity (members REST/GraphQL/MCP):** `lego/backend/internal/members/members_surface_parity_test.go` (`TestThreeSurfaceParity_UserIDAndEmail`) drives the same `Service` through its real `RegisterREST`/`GraphQLQuery`/`RegisterMCP` fragments and asserts identical `userId`/`email` for the same member. Green (t003).

**members ⇔ owners API parity:** not re-proven by a redundant integration test — it's a structural guarantee, verified by reading the composition root: `cmd/api/main.go:212` (`deps.WorkspaceStore = st`) and `:235` (`deps.MembersStore = st`) wire the **same** `*store.PGStore` instance to both features, so `OwnerIDForSubject` resolves against the one shared `owner_ids` table — the same subject always mints/returns the same `own-` id regardless of which feature asks. `internal/api/server.go`'s `Workspaces.Identities: d.Identities` and `Members.Identities: identityEmailLookup{d.Identities}` both derive from the single `Deps.Identities` (one Kratos-admin reader, `BEX_KRATOS_ADMIN_URL`) — same subject, same email lookup, same degradation (nil ⇒ omitted). Field naming already matches: owners' `renderTeamMember.UserID`/`.Email` (`workspaces/render.go`) and members' `MemberView.UserID`/`.Email` (`members/service.go`) both wire to `userId`/`email` on every surface.

**Team page vs Render's Team UI (t001 capture):** Render's dashboard shows each member primarily by email (with avatar/OTP/Google-account badges bex doesn't track at all — no per-user avatar/OTP-status surface anywhere in bex, not a members-specific gap). bex's Team page now matches on the identity axis: email primary, `own-` `userId` fallback, raw `subject` demoted to a muted secondary line (t004). Avatars/OTP badges were already out of scope per t004's own "Out of scope" section — not new drift, not filed as a follow-up.

**No drift accepted or found beyond what's already recorded in ADR018/ADR024** (t005): the `name` residual (no Kratos trait) and subject-keyed mutations (a deliberate bex-native contract, not Render's `userId` surface).

## Out of scope

- Fixing flagged drift that needs its own task/milestone.
