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-36

1.3 Projects

Done
Description

A workspace contains projects. Each project has a name, slug, and identifier (e.g., PROD). A workspace holds many projects (the project switcher lists them); the app scopes to one active project at a time at the UI level, and the non-unique workspaceId FK accommodates as many projects as a workspace needs — no per-workspace cap. Cross-project views that span projects at once (unified search / reporting) belong to the Search, reporting & admin epic (Epic 6).

Prerequisites: Story 1.2 (Workspaces) must be complete — project FKs against Workspace with onDelete: Cascade, the project RLS policies key off the same app.workspace_id session GUC that 1.2.3 established, and the active-project selection rides on the existing WorkspaceMembership row. Story 1.0.5 (Design system) must be complete before 1.3.3 (mockups) and 1.3.4 (UI) — those compose the canonical Button / Input / Card / Modal / Popover primitives (Popover shipped in 1.2.6).

Verification

  • project table: id (cuid), workspaceId (FK, onDelete: Cascade), name, slug (workspace-unique), identifier (3-5 chars uppercase, workspace-unique), lastWorkItemNumber (int default 0 — the per-project key counter), createdAt, updatedAt, archivedAt (nullable — soft-delete/archive, never a hard delete that would orphan work-item history in 1.4+).
  • Project identifier is workspace-unique and used as the prefix for work-item keys (e.g., PROD-42). Auto-generated from the name (uppercased, alphanumeric, 3-5 chars) with a numeric collision suffix; the user can override at creation.
  • Work-item-key allocation is gap-free and per-project: a repository method increments project.lastWorkItemNumber via UPDATE … RETURNING inside the caller's transaction (NOT a Postgres SEQUENCE — sequences are per-DB-object, leak on rollback, and would need one per project). Story 1.4 calls this when inserting a work item.
  • Active project stored per-workspace-member on WorkspaceMembership.activeProjectId (nullable FK, onDelete: SetNull) — determines what the member sees on landing. Switching projects updates this, mirroring the workspace_id cookie pattern from 1.2.6.
  • Postgres RLS on project: a row is visible/writable only when its workspaceId matches the active app.workspace_id GUC — the same structural gate 1.2.3 applied to workspace-scoped tables. Cross-workspace access is structurally impossible at the DB layer, not just the app layer.
  • Creating a project requires active workspace membership; archiving/deleting requires the typed-name double-confirmation modal (same pattern as workspace delete in 1.2.6).
  • Project creation flow: a modal with name + auto-generated identifier (overridable); on success the new project becomes the member's active project. If the member has no projects in the active workspace, a "Create your first project" empty state appears instead of a project view.
  • Project switcher in the top-nav (composed alongside the workspace switcher from 1.2.6): lists the workspace's projects with a check on the active one + a "Create project" entry. Selecting sets activeProjectId and re-renders.
  • 4-layer rule respected (per motir-core/CLAUDE.md): route/Server-Action → service → repository → Prisma; writes go through repo methods requiring tx; services own transactions + DTO mapping. All quality gates green; multi-tenant isolation proven by E2E + direct-DB RLS test (1.3.6).
Status
Done
Type
Story