---
id: w6/m7/t002
title: Members userId = the member's own- id across REST/GraphQL/MCP (Render parity for identifiers)
worker: worker6
status: done
estimate: 30m
depends_on: [w6/m7/t001]
---

## Objective

Report each workspace member's `userId` as their stable, opaque `own-` id,
matching Render, instead of the raw Kratos subject bex leaks today. This is the
**one real parity fix** in this milestone.

## Context

- `lego/backend/internal/workspaces/render.go` — `toRenderTeamMembers` sets `UserID: m.Subject` (the raw Kratos/Hydra subject, a UUID-ish string). Render's member `userId` is an opaque `own-` id (docs/render-artifacts/owners-api.md, retrieve-owner-members).
- Uses t001's `GetOrCreateOwnerID(subject)`; the mapping must be stable so a member's id doesn't churn between calls.
- Three-adapter parity: REST `/v1/owners/{id}/members`, the GraphQL members field, and the MCP members surface must all map identically (`docs/ADR006-bex-api.md`, `internal/api/CLAUDE.md`).

## Steps

1. In the members serialization, map `m.Subject → own-` id via the t001 resolver; emit that as `userId`.
2. Apply the same mapping in the GraphQL and MCP member shapes so all three agree.
3. Keep email/role (uppercase enum)/status unchanged.

## Files

- `lego/backend/internal/workspaces/render.go`
- `lego/backend/internal/workspaces/{graphql,mcp}.go` (member shapes, if they expose userId)
- `lego/backend/internal/workspaces/service.go`

## Acceptance criteria

- [ ] `GET /v1/owners/{id}/members[].userId` is an `own-` id, stable across calls
- [ ] GraphQL + MCP member identifiers match the REST mapping
- [ ] Email/role/status unchanged

## Out of scope

- Invite/member mutation (that's `w4/m12`); this is the read shape only.
- Owner-object `type` and peer `own-` resolution — see the parity task's note on why those are deliberately **not** implemented.
