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.
MemberRole enum + ProjectMembership + project.accessLevel @default(open) added in one Prisma migration; prisma migrate dev applies cleanly on a fresh DB and is idempotent.WorkspaceMembership.role values migrate into the enum (owner→owner, else→member); existing projects backfill to open (no lockout); ProjectMembership is RLS-forced + tenant-scoped (mirror WorkspaceMembership).prisma generate types the new model/enums; a vitest (real Postgres) asserts a project defaults to open and a ProjectMembership round-trips under RLS.prisma/schema.prisma — WorkspaceMembership (Story 1.2, the role string + RLS pattern to mirror) + Projectmotir-core/CLAUDE.md — one migration, application-seeded data, RLS-forced tenant tables