MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-30

1.2.2 Schema: Workspace + WorkspaceMembership tables (+ subtask_pr_merge_mode column)

Done
Description

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) and WorkspaceMembership (id cuid, userId, workspaceId, role string default 'member', createdAt, updatedAt; unique [userId, workspaceId]; FKs both onDelete: Cascade).
  • Migration add_workspaces generated 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_mode is a Postgres enum: SubtaskPrMergeMode { auto, manual, review_on_fail }. The review_on_fail value 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.ts exports: 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.ts cover: 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.prisma updated 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
Status
Done
Type
Sub-task