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.
projectMembersService add/remove/setRole + setAccessLevel exist, own their transaction, validate (add target must be a workspace member; last-admin guard on remove/role-change), return DTOs; setting private seeds current viewers as members.lib/services/workspacesService.ts addMember/removeMember/listMembers (Story 1.2) — the membership service precedent to mirror at project scope; the last-member guard patternProjectMembership + accessLevel); motir-core/CLAUDE.md (4-layer)