6.4.4 Project members + access API — manage memberships, roles, access level (project-admin-gated)
Estimate: 24m · Depends on: 6.4.2
The write path for project membership + access, 4-layer (Route → Service → Repository → Prisma) with the explicit workspace gate (finding #26) + a project-admin check. projectMembersService: addMember(projectId, userId, role) (the user must be a workspace member), removeMember, setRole, and setAccessLevel(projectId, level). Setting a project private seeds its currently-eligible viewers as member rows (so the owner + current users aren’t locked out — the Jira "go private → keep current people" shape). All gated to project admin (or workspace owner/admin). Routes: …/projects/[key]/members (GET/POST/PATCH/DELETE) + PATCH …/projects/[key]/access; HTTP-only, typed-error→status.
Acceptance criteria
projectMembersServiceadd/remove/setRole +setAccessLevelexist, own their transaction, validate (add target must be a workspace member; last-admin guard on remove/role-change), return DTOs; settingprivateseeds current viewers as members.- Routes are HTTP-only (one service call, typed-error→status: 400 invalid, 403 not project admin, 404 missing); project-admin (or workspace owner/admin) gated.
- Vitest (real Postgres) covers add/remove/setRole, the go-private seeding, the last-admin guard, and a non-admin denial.
Context refs
lib/services/workspacesService.tsaddMember/removeMember/listMembers(Story 1.2) — the membership service precedent to mirror at project scope; the last-member guard pattern- Story 6.4.2 (
ProjectMembership+accessLevel);motir-core/CLAUDE.md(4-layer)