Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-157Done

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). type is a Prisma enum BoardType { 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). projectId is 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). boardId FK with onDelete: Cascade. position is Decimal(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. wipLimit is Int? (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. columnId FK → board_column (onDelete: Cascade); statusId FK → 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 + the BoardType enum added in ONE Prisma migration; prisma migrate dev applies cleanly against a fresh DB.
  • RLS enabled + FORCED on all three tables; the app.workspace_id + app.system_admin GUC policy mirrors workflow_status (finding #33).
  • @@unique([boardId, statusId]) on board_column_status enforces ≤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.projectId is indexed but NOT unique (multiple boards per project is legal at the schema level).
  • board_column.position is Decimal(20,10) (finding #18); board_column.wipLimit is nullable Int.
  • An RLS-proof test (mirroring tests/jobs/rls.test.ts / the 2.2.1 workflow RLS test) under SET LOCAL ROLE prodect_app: workspace A sees only its own board rows; cross-workspace SELECT returns 0; an INSERT carrying a foreign workspaceId is rejected.
  • No SQL INSERT for default rows in the migration (seeding is application-layer, 3.1.2).

Context refs

  • prisma/schema.prisma — Story 1.3 project, Story 1.4 work_item, Story 2.2 workflow_status / workflow_transition / StatusCategory
  • Story 2.2.1’s workflow_status migration — the canonical RLS + app.system_admin escape-hatch shape this Subtask mirrors
  • Story 1.4.5’s FORCE ROW LEVEL SECURITY migration on work_item
  • tests/jobs/rls.test.ts — the role-switch RLS-proof harness
  • motir-core/CLAUDE.md — 4-layer rule, repo-write contract
  • Finding #18 — Decimal(20,10) position-column shape; finding #33 — GUC namespace