ADRs
ADR 0066 — Server persistence for Workflow composition artboards
  • 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 by apply-one <tag> if the CI migrate push can'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).