---
id: w6/m10/t002
title: "Backend: enrich members.MemberView with userId (own- id) + email via OwnerIDForSubject + identity lookup"
worker: worker6
status: done
estimate: 45m
depends_on: [w6/m10/t001]
---

## Objective

`members.Service.List` returns each member's opaque `own-` id (`userId`) and `email` alongside the existing `subject`/`role`/`createdAt`, using the exact enrichment the owners API already performs (`workspaces.ListMembers`, `lego/backend/internal/workspaces/service.go:318-350`): `OwnerIDForSubject` for the own- id, an identity lookup for email. When the identity reader is unwired (`BEX_KRATOS_ADMIN_URL` unset) or a lookup misses, email degrades to empty — the documented honest-omit contract — and the request still succeeds.

## Context

- `lego/backend/internal/members/service.go:125` — `MemberView{Subject, Role, CreatedAt}`; `memberView()` at :141 is the single projection point; `List` at :177.
- `lego/backend/internal/store/workspaces.go:201` — `PGStore.OwnerIDForSubject` (mints on first sight); add it to the `MembersStore` interface (`members/service.go:87`), `*store.PGStore` already satisfies it.
- Identity lookup: `workspaces.IdentityReader`/`IdentityAttrs` (`workspaces/service.go:124-137`) with impl `workspaces.KratosIdentities` (`workspaces/kratos.go`). `members` cannot structurally share it (named return type differs across packages) — give `members` its own narrow seam (e.g. `LookupEmail(ctx, subject) (string, bool)` or a local reader interface) and adapt in the composition root.
- Wiring site: `lego/backend/internal/api/server.go` — `Deps.Identities workspaces.IdentityReader` (:180) already exists and feeds `Workspaces`; `Members` is constructed at :239. Add the adapter there.
- Enrichment applies to `List` (the read); mutations keep their t001-decided keying.

## Steps

1. Extend `MembersStore` with `OwnerIDForSubject(ctx, subject) (string, error)`.
2. Add the identity seam to `members.Service` (nil ⇒ email omitted, mirroring `workspaces.Service.Identities`).
3. Extend `MemberView` with `UserID string \`json:"userId"\`` and `Email string \`json:"email"\`` (plus `Name` only if t001 decided to emit it); update `memberView()`/`List` to populate them; update the `MemberView` doc comment (it currently records the parity drift this task removes).
4. Wire the adapter in `internal/api/server.go` from `d.Identities`.
5. `cd lego/backend && go build ./... && go test ./...`.

## Files

- `lego/backend/internal/members/service.go`
- `lego/backend/internal/api/server.go`
- `lego/backend/cmd/api/` (only if wiring needs it)

## Acceptance criteria

- [ ] `List` returns `userId` + `email` per member when store + identities are wired (unit test with fakes).
- [ ] Nil identity reader / lookup miss ⇒ empty email, no error (the owners-API precedent).
- [ ] Existing members tests still green.

## Out of scope

- Surface fragments (t003) and dashboard (t004).
- Re-keying mutations by userId (t001's verdict governs; drift goes to t006).
