1.3 Projects
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
projecttable: 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.lastWorkItemNumberviaUPDATE … RETURNINGinside 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 theworkspace_idcookie pattern from 1.2.6. - Postgres RLS on
project: a row is visible/writable only when itsworkspaceIdmatches the activeapp.workspace_idGUC — 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
activeProjectIdand re-renders. - 4-layer rule respected (per
motir-core/CLAUDE.md): route/Server-Action → service → repository → Prisma; writes go through repo methods requiringtx; services own transactions + DTO mapping. All quality gates green; multi-tenant isolation proven by E2E + direct-DB RLS test (1.3.6).