1.2.2 Schema: Workspace + WorkspaceMembership tables (+ subtask_pr_merge_mode column)
Estimate: 18m · Depends on: 1.1.3
Add the two tables that make multi-tenancy real: Workspace (the tenant boundary) and WorkspaceMembership (the join table that lets users belong to one or more workspaces). Generate the Prisma migration; verify the FKs cascade correctly. No application logic in this Subtask — schema only. RLS policies land in 1.2.3, the middleware that sets the session GUC lands in 1.2.3 too.
Why a join table, not User.workspaceId single-FK: the Story-level AC says "each user belongs to one or more workspaces." A single-FK shortcut would contradict the AC and force a migration in a later Story when the switcher needs real data to switch between. Per notes.html mistake #28: pick the durable industry-standard shape, never the shortcut. WorkspaceMembership is the standard B2B SaaS shape (Linear, Notion, GitHub all use it).
Why an explicit role column despite single-role-in-v1: same principle. Adding the column now with a 'member' default costs zero runtime; omitting it would force every existing row to gain it in a later migration when RBAC arrives, plus a sweep of every authorization check to learn about roles. The column exists durably; the enforcement gate (a role-based policy engine) is a later Story. Story-level AC bullet 5 explicitly: "v1 supports one role (member)" — that names the enforcement state, not the schema state.
Why subtask_pr_merge_mode lives on Workspace, not User or Project: the planner decision applies per-workspace (a single user might belong to a developer team workspace with manual and a non-technical team workspace with auto). Story 1.4 § Merge modes documents the consumer model. Adding the column now means Story 1.4 doesn't have to schema-migrate when it lands.
What you'll do: Extend prisma/schema.prisma with the two models (verbatim shapes below). Run pnpm prisma migrate dev --name add_workspaces. Verify the cascade behavior with a smoke test: create a user, create a workspace, create a membership, delete the workspace → membership row must be gone; delete the user → membership row must be gone. Add a lib/workspaces/repo.ts with the minimal CRUD primitives (createWorkspace, addMember, removeMember, findUserWorkspaces, findMembership) — direct-DB helpers for the application layers landing in 1.2.4 / 1.2.5 / 1.2.6.
Acceptance criteria
- Two new Prisma models exist:
Workspace(id cuid, name string, slug string @unique, subtask_pr_merge_mode enum default 'manual', createdAt, updatedAt) andWorkspaceMembership(id cuid, userId, workspaceId, role string default 'member', createdAt, updatedAt; unique [userId, workspaceId]; FKs bothonDelete: Cascade). - Migration
add_workspacesgenerated and applies cleanly against a fresh DB. The migration is reversible (Prisma generates the down-migration automatically; verify it executes without error against a populated DB during local smoke). subtask_pr_merge_modeis a Postgres enum:SubtaskPrMergeMode { auto, manual, review_on_fail }. Thereview_on_failvalue exists in the enum but isn't exposed in the settings UI yet (Story 1.4 documents it as deferred); shipping the enum value now avoids a schema migration when it's exposed later.lib/workspaces/repo.tsexports:createWorkspace({ name, ownerUserId })(creates Workspace + initial Membership in a transaction),addMember,removeMember,findUserWorkspaces(userId),findMembership(userId, workspaceId). All functions normalize the slug from the workspace name (lowercase, hyphenate, suffix with random 4-char suffix on collision).- Vitest integration tests in
tests/workspaces-repo.test.tscover: createWorkspace happy path, slug-collision suffix behavior, cascade-on-Workspace-delete, cascade-on-User-delete, unique-constraint on (userId, workspaceId) returns a typed error not a generic Prisma error. - All 4 quality gates green:
pnpm typecheck,pnpm lint,pnpm format:check,pnpm build. Test suite green (existing 39 tests from Story 1.1 + new tests from this Subtask). - Schema docstring at the top of
schema.prismaupdated to mention workspaces (just like Story 1.1's auth-tables docstring documents that schema layer).
Context refs
prisma/schema.prisma— current schema (User + Account + Session + Verification from Story 1.1)lib/users/repo.ts— the repo-layer pattern this Subtask mirrors- Story 1.4 § Merge modes — documents the consumer of
subtask_pr_merge_mode - notes.html mistake #28 — durable-shapes-no-shortcuts rule (cited for join-table-not-single-FK, role-column-now-not-later)
- Prisma docs (fetched at prompt-gen time): cascade behaviors, enum types, unique constraints, transaction patterns