Skip to content

ADR-0013 — Loro over Automerge for collaborative config editing

Hakiri’s day-1 team product (M1) requires multiplayer editing of pipeline config across two surfaces — an Electron desktop app and a web UI (13-team-surfaces.md). Concurrent edits to the same pipeline (Alice tweaks the schedule, Bob renames the destination) are normal, not exceptional. Three families of approach exist:

  1. Pessimistic locking — kills the multiplayer feel and breaks offline editing for laptops.
  2. Optimistic concurrency with rejection — produces a “your edit was rejected, please refresh” UX that’s painful at multiplayer cadence.
  3. CRDT — concurrent edits merge automatically; same-field conflicts surface as structured UI affordances.

Given the offline-first requirement (laptop daemons may be offline for hours) and the multiplayer UX target, only CRDT survives. The remaining question is which CRDT library.

Three serious contenders:

  • Automerge — JSON-shaped CRDT, mature (production deployments since ~2018), Rust core (automerge-rs) with TypeScript bindings, designed by Martin Kleppmann and the Ink & Switch group.
  • Yjs — most popular collaborative-editing CRDT, optimized for rich-text editing (CodeMirror / ProseMirror / Slate integrations), JavaScript-first with a Rust port (yrs).
  • Loro — newer Rust-native CRDT library (~2023, post-1.0), designed for structured documents and high-performance sync, JS bindings via WASM, same local-first lineage as Automerge.

This decision is orthogonal to ADR-0005, which rejected CRDTs for the data sync layer (run files, snapshots, cursors). That decision stands. The CRDT here applies only to the config editing surface; daemons never speak CRDT and never import the loro crate.

Use Loro for the collaborative config doc held in the team’s Durable Object (CF substrate) or hakiri-control Rust daemon (self-hosted substrate), replicated to all connected Electron / web clients over WebSocket.

The doc model leverages Loro’s LoroMap, LoroMovableList, LoroText, and LoroTree types — see 14-collab-config.md § Loro doc shape. The daemon side reads materialized TOML snapshots from R2 unchanged from the existing reconciliation path in 03-pipelines.md.

Positive

  • Rust-native fit. Loro is Rust-first; hakiri-control imports the loro crate directly. JS bindings are derived from the same core via WASM. One source of truth for sync semantics across server (control plane) and client (Electron + web).
  • LoroMovableList for transform reordering. Drag-to-reorder produces stable move ops, not delete-then-insert. Automerge does not have a native move type; emulating it is the documented source of conflict-resolution pain in Automerge-based apps.
  • LoroTree for pipeline grouping. Reparenting in a CRDT tree without breaking sibling order is non-trivial; Loro ships it. We get pipeline folders / nested groups without rolling our own.
  • Performance headroom. Loro benchmarks 10–100× over Automerge on op-heavy workloads. For hakiri’s manifest sizes today this is not load-bearing, but as teams scale past hundreds of pipelines it gives us slack we’d otherwise burn on Automerge.save() performance hacks.
  • Compact binary encoding. Loro’s columnar encoding produces smaller wire frames and smaller stored doc binaries — relevant for both DO storage cost and laptop-bandwidth scenarios.
  • Aligns with the local-first pillar. Loro is built explicitly for local-first software, same Ink & Switch lineage as Automerge but with newer design choices and a Rust-first architecture.

Negative

  • Younger ecosystem. Loro is ~2 years old at the time of decision; smaller community, fewer Stack Overflow answers, less production track record than Automerge. Mitigated by a small wire-protocol surface (we don’t depend on many features) and a v0 perf spike to validate against representative workloads before locking in.
  • Awareness/presence API newer. Yjs has Awareness as a battle-tested first-class API; Loro’s is more recent. We’ll layer “Bob is editing pipeline X” presence on Loro’s hooks in hakiri-control rather than expect a turnkey solution.
  • Smaller editor-binding ecosystem. Yjs has plug-and-play integrations for CodeMirror, ProseMirror, Slate, Quill. Loro has fewer. Acceptable because hakiri’s config UI is form-shaped, not rich-text-shaped — we don’t need editor bindings.
  • Schema migration story TBD. If we change the doc shape, we write migration functions in hakiri-control. Automerge has the same story (CRDTs generally don’t have schema migration), but the broader Loro ecosystem has fewer published patterns to crib from.

Neutral

  • Both Loro and Automerge serialize to deterministic binary updates over WebSocket. Migration from Loro to Automerge (or vice versa) is contained — the doc model translates, only the wire-protocol bytes change. We retain the option.

Automerge. Mature, larger production track record, better-documented awareness layer. Rejected for:

  • No MovableList — transform reordering needs hand-rolled OT, with documented conflict pain in production apps.
  • Rust port is secondary to the JS implementation; Loro’s Rust-first design is a better fit for hakiri-control.
  • Performance gap is significant at scale; we’d be optimizing around it within a year.

Automerge stays as the fallback if a Loro production bug bites — the doc model translates with modest effort.

Yjs / yrs. Best ecosystem, mature awareness API. Rejected for:

  • Optimized for rich-text shape (CRDT’d character streams). Hakiri config is structured records, not prose.
  • No native tree type for hierarchical groups.
  • yrs (the Rust port) lags Yjs in features; we’d be on the Rust port’s trailing edge.

No CRDT, optimistic concurrency. Rejected — multiplayer editing UX requirement is load-bearing for the team product. Offline-first laptops cannot rely on “refresh and retry.”

Pessimistic locking. Rejected — kills the multiplayer feel; breaks offline editing entirely.

Custom Operational Transform. Rejected for the same reasons as the v0 CRDT decision in ADR-0005 — implementation cost is large, the mature Rust ecosystem is absent, and CRDTs are strictly more general.

  • Daemons MUST NOT depend on Loro. Daemons read materialized TOML from R2. The hakiri-runtime crate has no loro dependency. Enforce via a CI cargo dep audit.
  • Pre-M1 perf spike. Synthetic 200-pipeline × 20-transform doc; 5 simulated concurrent editors for 30 min; measure doc size, sync message size, merge latency p50/p95. Acceptance gates documented in pm/roadmap.md M1 success criteria.
  • Wire-protocol versioning. The DO advertises the Loro version it speaks; clients with mismatched versions get a hard error and a “please update Hakiri” prompt. No silent partial compatibility.
  • Migration-out tested in CI. A CI job exports a Loro doc to a generic JSON form and re-imports into an Automerge-shaped scratch doc, proving the fallback path is not theoretical.