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_updatealready 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:
- 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.
- Reordering by two users at once. With LWW, the second reorder discards the first. With Yjs, the moves can converge via fractional indices.
- 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
- Bundle size. Yjs + y-protocols + provider = ~80-150KB to the client.
- 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.
- Persistence complexity. DO SQLite + D1 + Yjs snapshots is more moving parts than D1 + JSON.
- Debuggability. "Why did this field have value X?" is harder to answer with Yjs ops than with a SQL row.
- 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.
- 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:
- 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.
- Harden field-level merge — make sure every
settings_updateand equivalent path correctly merges nested keys (already partially done; audit for completeness). - Build agent API parity properly — every GUI action has a REST endpoint. This is needed regardless of CRDT.
- Add MCP server so Claude/codex/etc. can drive the tool. Higher product value than CRDT for any near-term use case.
- Revisit CRDT in 3-6 months with actual collision data. If real, integrate Yjs for
cv_profileandcl_profileonly. 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
- Read this section. Push back if anything's off.
- If you agree: pick ONE of (agent API parity / MCP server / collision instrumentation) as the next architectural piece. They're separable, all valuable.
- 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."