ADR-012: CRDT-Backed Real-Time Multi-User Collaboration¶
Date: 2026-04-22
Status: Accepted
Decision-makers: FCC core maintainers · LAR (Lane-Router Architect) persona (UXBus integration)
Consulted: AURORA / Sky Parlour team (collab frontend patterns)
Informed: AOME admin-UI consumers · All fcc.api.collaboration clients
Context and Problem Statement¶
The CollaborationEngine shipped in v1.2.x supported human-in-the-loop
workflows: session lifecycle, scoring, approval gates, handoff
protocols. It was designed for turn-taking collaboration — one
participant edits, others review — backed by a single shared
SharedContext key-value store with optimistic locking.
The v1.5.0 scope required real-time multi-user collaboration: multiple authors editing the same session simultaneously, with every edit visible to every other participant within ~100 ms, and no single point of contention. Turn-taking + optimistic locking could not meet that requirement.
Three design regions had to be chosen:
- Concurrency-control primitive. Operational Transformation (OT), Conflict-free Replicated Data Types (CRDT), or pessimistic central-lock?
- Transport. WebSocket, SSE, or polling?
- State propagation. Full state snapshots, incremental patches, or tombstone-annotated diffs?
The problem was how to add real-time multi-user collaboration without
making collaboration require a central server process (FCC must remain
pip install-runnable) and without coupling FCC to a heavy external
CRDT library.
Decision Drivers¶
- No central server required. FCC must work with the in-process WebSocket bridge; a central authoritative server must be an optional deployment mode, not a requirement.
- Deterministic for tests. Multi-user merge semantics must reproduce deterministically under seeded clocks.
- Optional heavy dependency. A production-grade CRDT library
(
y-py) should be optional; pure-Python fallback must work. - UXBus integration. Presence + edit events must route through the
UXBus (per ADR-006) with
lane: uxclassification. - Late-joiner support. Participants joining mid-session must receive full state sync to catch up.
Considered Options¶
- Operational Transformation (OT). Classic for collaborative editors (Google Docs history). Requires a central authoritative server to linearise operations; incompatible with the no-central-server driver.
- Central pessimistic lock. Simple but serialises all edits; doesn't meet the ~100 ms visibility target.
- CRDT with LWW (Last-Writer-Wins) + Lamport clock. Classic CRDT design; converges under any delivery order; deterministic under seeded replica IDs; no central server required.
- CRDT with y-py (Yjs port) as the backend. Production-grade, battle-tested CRDT library; richer merge semantics (Y.Text, Y.Array, Y.Map) but a heavy optional dep.
- Hybrid: Protocol + pluggable backend. Define a
CRDTBackendProtocol; ship anInMemoryBackend(LWW + Lamport, pure-Python, default) and an optionalYPyBackendfor production deployments.
Decision¶
We adopt option 5: a CRDTBackend Protocol with two concrete
implementations, plus a CollaborativeDocument facade and a
MultiUserSession engine, shipped in v1.5.0 Pillar D.
Implementation files:
src/fcc/collaboration/crdt.py(507 LoC) —CRDTBackendProtocol,InMemoryBackend(LWW + Lamport clock + replica-id tie-break),YPyBackend(optional via[collab-realtime]dep group),CollaborativeDocumentfacade (text + metadata + presence channels).src/fcc/collaboration/multi_user.py(458 LoC) —MultiUserSession(per-session CRDT + presence + UXBus events),MultiUserEngine(multi-tenant routing), wire-envelope codec (edit / presence / join / leave / ack / state).src/fcc/protocols/ws_bridge.py— extended withCollabWebSocketBridgeat/collab/{session_id}multiplex path; client tracking; broadcaster fan-out; late-joiner state sync.frontend/src/pages/Admin/CollabLive.tsx(328 LoC) — 2-pane collaborative editor admin demo at/admin/collab-live. UsesUxEventSSEClientfor presence + WebSocket for CRDT updates.
Concurrency-control design. Every edit carries a Lamport timestamp (logical clock) + replica ID. Merge resolution is LWW on the logical clock with replica-ID as the deterministic tie-break. This gives:
- Commutativity. Edits arriving in any order converge to the same state.
- Determinism. Under fixed replica IDs + clocks, the tests reproduce exactly.
- No central lock. Any replica can accept edits locally and broadcast; convergence is guaranteed.
Wire envelope. Six message types: edit, presence, join,
leave, ack, state. state is used by late joiners to receive
the full CRDT state; all others are incremental. Encoded as JSON for
debuggability; a binary encoding is a future optimization.
UXBus integration. Presence + edit events emit on the UXBus with
lane: ux classification per ADR-006. Admin UI + Sky Parlour can
subscribe via SSE or WebSocket.
y-py optional. YPyBackend is behind the [collab-realtime]
extras group. Default is InMemoryBackend; consumers requesting
production-grade CRDT opt in with pip install fcc[collab-realtime].
Consequences¶
Positive¶
- No central server required.
InMemoryBackend+ WebSocket bridge run in-process; FCC's standalone deployment story is preserved. - Deterministic tests. 95 tests reproduce exactly under seeded clocks + replica IDs.
- Optional production backend.
y-pyavailable for deployments that need battle-tested CRDT; pure-Python fallback for everyone else. - Late-joiner support.
stateenvelope gives new participants immediate catch-up; no replay of historical edits required. - Clean UXBus coupling. Presence + edit fan-out reuses the v1.4.2 dual-bus infrastructure; no new messaging primitives.
Negative¶
- CRDT mental model. LWW semantics can feel counterintuitive ("my edit got overwritten because the other replica's clock was ahead"). Mitigated by surfacing clock values in the envelope for debugging.
- Two backends to maintain.
InMemoryBackend+YPyBackendmust stay behavior-compatible. Mitigated by a shared Protocol + parameterized test suite. - JSON wire cost. JSON envelopes are debuggable but not bandwidth-optimal. Binary encoding is deferred.
Confirmation¶
- 88 Python tests + 7 vitest frontend tests (95 total) — 100% line + branch coverage on new modules.
CollabLive.tsxadmin demo exercises the full edit + presence + late-joiner flow end-to-end.- Both backends (
InMemoryBackend,YPyBackend) pass the same parameterized test suite. - CHANGELOG
[1.5.0]§Pillar D records the full delivery.
References¶
src/fcc/collaboration/crdt.py— CRDT primitivessrc/fcc/collaboration/multi_user.py— session enginesrc/fcc/protocols/ws_bridge.py—CollabWebSocketBridgefrontend/src/pages/Admin/CollabLive.tsx— admin demotests/test_crdt.py+tests/test_multi_user.py— test suites- ADR-006 — dual-bus event architecture (UXBus classification)
- ADR-010 — six-pillars context (Pillar D)
- CHANGELOG
[1.5.0]§Pillar D — Real-time WebSocket multi-user collaboration - Shapiro et al., Conflict-free Replicated Data Types (INRIA 2011) — CRDT foundation
- Lamport, Time, Clocks, and the Ordering of Events in a Distributed System (1978) — logical-clock foundation