Ships the durable workflow primitive every later issue surface reads from: each project owns an ordered set of statuses, each status carries a category (todo / in_progress / done) — the Jira-style three-bucket taxonomy — and a per-project set of legal transitions between them. New projects auto-seed the default workflow (To Do → In Progress → In Review → Done). After this Story, every work_item row's status string is resolvable to a typed status row, and Epic 3's boards (columns), Epic 6's reports (filters/groupings), and Story 1.4's isReady predicate (finding #21 — "terminal" generalizes from hardcoded 'done' to category = 'done') all read this single source of truth.
Prerequisites: Story 1.3 ships the project table and the projectsService.createProject transaction that seeds per-project state (the key counter). Story 1.4 ships work_item.status as a String column with 'todo' default; this Story does NOT change its type (kept as a string for portability) but adds the typed-status integrity layer via service-layer validation + an RLS-scoped lookup table. Story 2.1 ships the issuesService that this Story's transition validation hangs off. All work follows motir-core/CLAUDE.md's 4-layer architecture (Route → Service → Repository → Prisma). Per finding #26, every new route in this Story carries an explicit workspaceId gate at the application layer — RLS is the backstop, not the sole gate, because the dev/CI superuser bypasses RLS.
pnpm install && pnpm prisma generate && pnpm prisma migrate dev against a fresh local DB.pnpm test — vitest covers schema RLS, default seed, service read API, transition validation, the management writes (including atomic initial-status flip + delete protections), and the finding-#21 generalization.pnpm test:e2e --grep workflow-flow — the closing Playwright suite runs against the real stack./settings/project/[key]/workflow: verify all six default statuses (To Do / Blocked / In Progress / In Review / Done / Cancelled) and the fifteen transitions, rename one, add a custom status, add/remove a transition, flip policy mode, attempt a restricted-mode illegal status change on a real issue → typed-error toast.done AND cancelled terminal categories.status: 'blocked' (the status flag); on another, leave status todo but add a work_item_link.is_blocked_by edge to an open blocker. Both should show up in a "what's blocked?" filter / view (when those surfaces ship in Epic 2.5 / Epic 6); for this Story, just confirm both signals coexist without conflict — isReady returns false for the link-blocked one, and the status-blocked one is filterable by status.prodect_app, SET app.workspace_id = '<workspace-A>', query workflow_status + workflow_transition — see only workspace A's rows.