3.1.1 Schema — `board` + `board_column` + `board_column_status` mapping + RLS
Estimate: 20m · Depends on: 2.2.1, 1.3.1, 1.4.2
Add the three board tables to prisma/schema.prisma and ship one Prisma migration that creates all three plus their RLS policies, reusing the workspace-scoped app.workspace_id GUC pattern Story 2.2.1 established (mirrors job_run / finding #33). Every table carries an explicit workspaceId column + a projectId FK + the Story-1.2 RLS gate, so board data inherits tenant isolation by structure, not by joins.
Tables and key constraints:
board:(id, workspaceId, projectId, name, type, createdAt, updatedAt).typeis a Prisma enumBoardType { kanban, scrum }(the durable shape — Jira has both; the Scrum sprint-scoped view is Story 4.5 — moved to Epic 4 per mistake #32 — but the enum value exists now so 4.5 adds no enum migration).projectIdis a plain FK, NOT unique — one project may own many boards (multiple-boards-per-project is a non-breaking addition planned as Story 3.7; the seed creates exactly one default board).@@index([projectId])for the per-project board lookup.board_column:(id, workspaceId, projectId, boardId, name, position, wipLimit, createdAt, updatedAt).boardIdFK withonDelete: Cascade.positionisDecimal(20,10)matching the work-item / workflow-status column-shape rule (finding #18) — the same fractional-index ordering, so column reorder (a later admin action) needs no new mechanism.wipLimitisInt?(nullable) — the column exists now so the WIP work in Story 3.3 adds no migration; enforcement / over-limit warnings are 3.3, not this story (this story only persists the column).@@index([boardId, position])for ordered column reads.board_column_status:(id, workspaceId, projectId, boardId, columnId, statusId, createdAt)— the column ↔ status mapping.columnIdFK →board_column(onDelete: Cascade);statusIdFK →workflow_status(onDelete: Cascade).@@unique([boardId, statusId])enforces a status maps to ≤1 column per board (many statuses MAY map to one column — the Jira "merge In Progress + In Review" shape — but a status is never in two columns at once). A project status with NO row here is unmapped (surfaced by the 3.1.4 projection, not shown as a column).@@index([columnId])for the per-column status read.
RLS: all three tables enable RLS + FORCE ROW LEVEL SECURITY (so even the table owner is gated, per Story 1.4.5). One policy per table covering all four verbs: USING (workspace_id = current_setting('app.workspace_id')::uuid) + the system-admin escape hatch OR current_setting('app.system_admin', true) = 'true' (finding #33), mirroring workflow_status exactly.
What this does NOT do: seed any rows (the default board is 3.1.2's job — application code under the prodect_app role with the workspace GUC set, never a SQL INSERT in the migration); add any read/write service or repository (3.1.3+); or change work_item (card placement is derived from its existing status + position, no new column).
Acceptance criteria
board+board_column+board_column_status+ theBoardTypeenum added in ONE Prisma migration;prisma migrate devapplies cleanly against a fresh DB.- RLS enabled + FORCED on all three tables; the
app.workspace_id+app.system_adminGUC policy mirrorsworkflow_status(finding #33). @@unique([boardId, statusId])onboard_column_statusenforces ≤1 column per status per board; a second mapping of the same status to a different column on the same board is rejected by a constraint violation.board.projectIdis indexed but NOT unique (multiple boards per project is legal at the schema level).board_column.positionisDecimal(20,10)(finding #18);board_column.wipLimitis nullableInt.- An RLS-proof test (mirroring
tests/jobs/rls.test.ts/ the 2.2.1 workflow RLS test) underSET LOCAL ROLE prodect_app: workspace A sees only its own board rows; cross-workspace SELECT returns 0; an INSERT carrying a foreignworkspaceIdis rejected. - No SQL
INSERTfor default rows in the migration (seeding is application-layer, 3.1.2).
Context refs
prisma/schema.prisma— Story 1.3project, Story 1.4work_item, Story 2.2workflow_status/workflow_transition/StatusCategory- Story 2.2.1’s
workflow_statusmigration — the canonical RLS +app.system_adminescape-hatch shape this Subtask mirrors - Story 1.4.5’s
FORCE ROW LEVEL SECURITYmigration onwork_item tests/jobs/rls.test.ts— the role-switch RLS-proof harnessmotir-core/CLAUDE.md— 4-layer rule, repo-write contract- Finding #18 —
Decimal(20,10)position-column shape; finding #33 — GUC namespace