Skip to content

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:

  1. Concurrency-control primitive. Operational Transformation (OT), Conflict-free Replicated Data Types (CRDT), or pessimistic central-lock?
  2. Transport. WebSocket, SSE, or polling?
  3. 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: ux classification.
  • Late-joiner support. Participants joining mid-session must receive full state sync to catch up.

Considered Options

  1. 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.
  2. Central pessimistic lock. Simple but serialises all edits; doesn't meet the ~100 ms visibility target.
  3. 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.
  4. 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.
  5. Hybrid: Protocol + pluggable backend. Define a CRDTBackend Protocol; ship an InMemoryBackend (LWW + Lamport, pure-Python, default) and an optional YPyBackend for 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) — CRDTBackend Protocol, InMemoryBackend (LWW + Lamport clock + replica-id tie-break), YPyBackend (optional via [collab-realtime] dep group), CollaborativeDocument facade (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 with CollabWebSocketBridge at /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. Uses UxEventSSEClient for 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-py available for deployments that need battle-tested CRDT; pure-Python fallback for everyone else.
  • Late-joiner support. state envelope 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 + YPyBackend must 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.tsx admin 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 primitives
  • src/fcc/collaboration/multi_user.py — session engine
  • src/fcc/protocols/ws_bridge.pyCollabWebSocketBridge
  • frontend/src/pages/Admin/CollabLive.tsx — admin demo
  • tests/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