- Date: 2026-07-14
- Status: Accepted
- Builds on: ADR 0031 (Workflow canvas) + its Studio-in-Workflow addendum (Phase 1/2). Phase 3 of that work.
Context
Studio-in-Workflow Phase 1/2 shipped editable composition artboards, but both the
artboard list and each artboard's tree live in localStorage
(use-workflow-artboards.ts, use-artboard-tree.ts). That means work is
per-browser, non-collaborative, and — crucially — a built component is a dead
end: it can't be reused, published, or opened on another device. Server
persistence is the prerequisite for every downstream step (save-to-Library,
publish → code).
Decision
A dedicated table workflow_artboard, project-scoped, one row per artboard,
holding the metadata AND the tree. Chosen over reusing
sorgente.composition_instance (which is maintainer-only, keyed by userId, no
project_id — ADR 0052) because artboards are ordinary per-project user data and
must be owner-scoped. A dedicated table also gives the list, per-artboard
position (future free-move, ADR 0031 §9), and ordering a home — not just trees.
Schema (packages/db/src/schema.ts)
workflow_artboard
project_id text FK → project.id ON DELETE CASCADE
artboard_id text (client key: "main", "ab-<ts>")
title text
tree jsonb (ponte LayoutNode; typed `unknown` like composition_instance)
position jsonb ({x,y} | null — reserved for free-move)
sort_order integer default 0
created_at / updated_at timestamptz default now()
PRIMARY KEY (project_id, artboard_id)Mirrors captured_page (public schema, project FK, cascade). Migration via
drizzle-kit generate (timestamp-prefixed, appends journal idx 40).
API (apps/api/src/workflow-artboard.ts, mounted post-auth in index.ts)
Captured-pages pattern — requireOwnedProject(c, projectId) gate,
getDB(c.env.DATABASE_URL):
GET /api/v1/projects/:projectId/workflow-artboards→{ items: [full rows] }(trees included; realistic artboard trees are small, and every visible artboard needs its tree to render — one fetch beats N+1).PUT /api/v1/workflow-artboards→ upsert on(project_id, artboard_id).DELETE /api/v1/workflow-artboards?project=&id=→ idempotent delete.
500k-char tree cap (mirrors sorgente). No apply-log (that's sorgente-specific).
Frontend
Because the list GET already returns trees, tree state is lifted into
useWorkflowArtboards (single source, no double-fetch): it owns the list, each
tree, add(), and a debounced saveTree(id, tree) (600 ms, mirrors
useStudioTree). use-artboard-tree.ts (the Phase-1 localStorage hook) is
removed; WorkflowCompositionArtboard becomes presentational (tree + active
onTreeChange) and registers on the studio-bus only while active — undo kept via a small in-component history. localStorage keys from Phase 1/2 are abandoned (no migration of local drafts — pre-server work was explicitly ephemeral).
Consequences
- Artboards survive reload / device / browser; foundation for save-to-Library + publish.
- One backend table + endpoint + one migration. Additive
CREATE TABLE— safe, non-destructive; the deploy-test journal-divergence gotcha (HANDOFF) is handled byapply-one <tag>if the CImigrate pushcan't run the full journal. - No collaboration yet (writes are last-write-wins per owner; no CRDT). Multi-user editing is a later concern.
Deferred
Rename UI; free-move (writing position); server-side ordering UI; Library
save; collaboration/CRDT; captured-node edit-by-rules (ADR 0031 §14).