Sync direction — analysis for next decision

Written 2026-04-25 (after the variantGroup refactor session). For Julian to read on return.

TL;DR

You said you're "not sure about the sync aspect." Here's the honest picture, the genuine tradeoffs, and a concrete recommendation.

Recommendation: Don't go full CRDT. Stay server-authoritative-with-broadcast for everything except the base CV/CL quarry, which gets Yjs only when (and if) you observe two collaborators actively editing the same field. Right now there's no evidence of that pressure; building CRDT infra ahead of demand is premature complexity.

If you only have 30 seconds: skip to The Recommendation.


What you have today

  • One Durable Object per workspace (CVWorkspace), pure WebSocket relay
  • No CRDT. Just message broadcasting. Conflicts resolve last-write-wins.
  • Optimistic UI — local writes apply immediately, broadcast for others
  • Field-level merging for nested JSON (settings) — already in code, prevents most concurrent-edit problems
  • Per-tab session ID for self-echo suppression (today's fix)
  • Offline write queue in localStorage, flushed on reconnect

What you decided in earlier conversation (now in memory)

  • CRDT scope is broad — humans + AI agents collaborate on every state, including jobs and tailored outputs (not just quarry)
  • Agent API parity is a product requirement (REST + likely MCP)
  • Presence is feature-complete — don't extend
  • "Link = identity" stays — no auth, no permission checks

The real question

Two architectures, both viable:

Path A — server-authoritative + broadcast (what you have, hardened)

  • Client writes → API endpoint validates + persists → broadcast to other clients via DO
  • Each mutation is a typed operation: POST /api/jobs/:id/score, PATCH /api/workspaces/:id/cv/:lang/sections/...
  • Concurrent writes: last-write-wins at field level. Field-level merging on settings_update already shipped.
  • Multi-editor on same document: works for non-overlapping edits (Tab A edits summary, Tab B edits experience — both land cleanly via field merge). Same-field collisions: last writer overwrites.
  • Agent API parity: trivially supported — agents call the same endpoints as the GUI.

Path B — CRDT-everything (the Yjs path)

  • Document state is a Y.Doc (Yjs CRDT). Mutations are Yjs ops applied locally and broadcast.
  • DO holds the canonical Yjs doc. Reconnecting clients sync via Yjs awareness.
  • Persistence: DO SQLite for hot snapshot, D1 for cold backup, R2 was rejected as overkill.
  • Concurrent writes: Yjs converges deterministically. Same-field edits merge by character-level CRDT (text) or LWW (Y.Map).
  • Agent API parity: agents either drive Yjs ops directly (heavier integration) or call REST endpoints that translate to Yjs ops (server adapter).

What CRDT actually buys you

Concrete cases where CRDT > LWW:

  1. Two people editing the same paragraph of a CV section simultaneously. With LWW, last save wins, the other's edit is lost. With Yjs Y.Text, characters merge.
  2. Reordering by two users at once. With LWW, the second reorder discards the first. With Yjs, the moves can converge via fractional indices.
  3. Offline edits over a long stretch. With LWW, conflict resolution on reconnect requires the user to pick. With Yjs, automatic merge.

Honest assessment of your actual usage:

  • Per the workflow you described: "mostly one worker per job, sometimes multiple convene." Same-field collisions on a single CV: rare.
  • For non-overlapping edits (different fields, different sections), the existing field-level merge already handles it.
  • For real-time co-editing of one paragraph: not a documented current need.

The CRDT case is "future-proofing for a usage pattern we don't yet see."

What CRDT actually costs

  1. Bundle size. Yjs + y-protocols + provider = ~80-150KB to the client.
  2. Conceptual surface. Every mutation is now a Yjs op, not a typed API call. Agents that call REST are translating to ops on the server. New developers (or agents) must understand Yjs semantics.
  3. Persistence complexity. DO SQLite + D1 + Yjs snapshots is more moving parts than D1 + JSON.
  4. Debuggability. "Why did this field have value X?" is harder to answer with Yjs ops than with a SQL row.
  5. The agent-API-parity-cost. Per the agent API memory: "anything a human can do, an agent must be able to do." With CRDT, the natural API is "apply this Yjs op." That's not a great API for agents — they'd prefer "mutate this field." So you'd build a REST translation layer on top of Yjs anyway. Two layers instead of one.
  6. Migration cost. Existing data is plain JSON. Initial conversion to Y.Doc runtime state is a one-time cost but adds boot complexity.

The decision lens

The only reason to invest in CRDT NOW is if same-field concurrent editing is a real product need today. If you're not observing collisions in the wild, building CRDT to handle a hypothetical case is the wrong order of operations.

What to do instead, in order:

  1. Instrument the existing realtime path. Log when two clients write to the same field within N seconds of each other. If it's <1% of writes, CRDT solves nothing real. If it's >10%, you have evidence.
  2. Harden field-level merge — make sure every settings_update and equivalent path correctly merges nested keys (already partially done; audit for completeness).
  3. Build agent API parity properly — every GUI action has a REST endpoint. This is needed regardless of CRDT.
  4. Add MCP server so Claude/codex/etc. can drive the tool. Higher product value than CRDT for any near-term use case.
  5. Revisit CRDT in 3-6 months with actual collision data. If real, integrate Yjs for cv_profile and cl_profile only. Keep jobs and settings server-authoritative.

The Recommendation

Path A, hardened. Skip Yjs. Build agent API + MCP. Let CRDT come if and when data justifies it.

Justification:

  • Your documented usage is mostly single-operator with rare convening. LWW + field merge handles this.
  • Agent API parity is a stated product requirement; CRDT doesn't move that forward, it complicates it.
  • Bundle size matters for the rehost. Adding ~120KB to the client when the existing solution covers 95% of cases is not a great trade.
  • "Premature CRDT" is the same family of mistake as "premature microservices" — solves problems you don't have, costs you ones you didn't.

What this means concretely

If you accept this recommendation:

What Status
Yjs / CRDT integration Don't build now. Park it.
Field-level merge on all settings_update paths Audit + harden. ~1 day.
Agent API endpoint parity Build out. Every GUI action gets a REST endpoint. ~1 week of work.
MCP server Build it. Wraps the REST API as MCP tools so Claude/codex can drive the tool. ~1 week.
Realtime collision instrumentation Add it. Log same-field concurrent writes. Lets you measure whether CRDT is needed. ~half day.
Rehost (skeleton branch) Independent decision. CRDT vs LWW doesn't change the rehost calculus.

What this DOESN'T mean

  • It doesn't mean abandoning the multi-user collaboration vision. LWW + field merge supports multi-user fine for the patterns you described.
  • It doesn't mean "no realtime sync" — you have that already, and it works.
  • It doesn't mean Yjs is wrong forever. Just wrong now, when there's no measured need.

What I'd suggest you do when you read this

  1. Read this section. Push back if anything's off.
  2. If you agree: pick ONE of (agent API parity / MCP server / collision instrumentation) as the next architectural piece. They're separable, all valuable.
  3. If you disagree and want CRDT now: tell me why (concrete use case where you've seen LWW fail). Then we can spec the integration honestly.

Either way, this analysis is now in .agent/runs/sync-direction.md and the corrected memory entries (project_careervector_crdt_scope.md) so the conversation can resume from a real place, not from "I'm not sure."

Source: wiki/content/archive/2026-05/agent-runs/sync-direction.md