Data model overview
Postgres via Drizzle; every tenant-owned table carries workspace_id and every
repository method takes it as a required first argument — queries physically cannot
forget the tenancy filter (DATA_ACCESS).
Core spine:
user ─ workspace_member ─ workspace ─ project ─ work_item ─ comment / attachment / activity │ └ parent_id (one-level sub-tasks) ├ workflow_state (categories: backlog/unstarted/started/completed/cancelled) ├ cycle (date-only timeboxes) └ saved_viewworkspace ─ objective / module (roadmap) · notification · audit_event · outbox_event · email_outbox · idempotency_keySemantics worth knowing before changing anything:
-
Identifiers (
SKY-42) mint fromproject.next_item_sequnder lock — unique, sequential, immutable. -
Revisions:
work_item.revisionbacks compare-and-set updates (409 with the authoritative body on staleness). Same contract on cycles. -
Soft deletes (
deleted_at) on items/comments; listings always filter them. -
Activity + outbox rows commit in the mutation’s transaction — proven by fault-injection tests.
-
Migrations live in
packages/db; CI runs both fresh-install and upgrade paths. -
Knowledge pages (PR-83) store a structured document, not HTML:
content_jsonis the source of truth andcontent_textis a server-extracted search projection — the client never supplies it, so the two cannot drift. Every block carries ablockIdnormalized server-side, which is what lets a task created from a paragraph survive later edits to it. Depth is computed, never stored; a move would leave a stored depth wrong. History is an append-only snapshot table with a unique(page_id, revision)index.
Full model: DATA_MODEL.