6.4.2 Schema — role enum + `ProjectMembership` + `project.accessLevel` (migration-aware)
Estimate: 30m · Depends on: 1.2
The data model for project gating, in ONE migration. (a) Formalize the role set — a MemberRole enum { owner, admin, member, viewer }; keep WorkspaceMembership.role (migrate the existing string column: owner→owner, everything else→member). (b) A ProjectMembership { userId, projectId, role: MemberRole } join (unique [userId, projectId], RLS-forced + tenant-scoped like WorkspaceMembership; cascade on project/user delete). (c) A project.accessLevel enum { open, limited, private } @default(open) — existing projects backfill to open so no one is locked out.
What this does NOT do: the enforcement gate (6.4.3), the management API (6.4.4), any UI (6.4.5/6.4.6), or the seed (6.4.7). It also does not build a full permission-scheme matrix — project roles + access level are the model; the browse/edit policy is computed in 6.4.3.
Acceptance criteria
MemberRoleenum +ProjectMembership+project.accessLevel @default(open)added in one Prisma migration;prisma migrate devapplies cleanly on a fresh DB and is idempotent.- Existing
WorkspaceMembership.rolevalues migrate into the enum (owner→owner, else→member); existing projects backfill toopen(no lockout);ProjectMembershipis RLS-forced + tenant-scoped (mirrorWorkspaceMembership). prisma generatetypes the new model/enums; a vitest (real Postgres) asserts a project defaults toopenand aProjectMembershipround-trips under RLS.
Context refs
prisma/schema.prisma—WorkspaceMembership(Story 1.2, therolestring + RLS pattern to mirror) +Projectmotir-core/CLAUDE.md— one migration, application-seeded data, RLS-forced tenant tables