Skip to content

Repository files navigation

Braid

A real-time collaborative whiteboard where the interesting part isn't drawing — it's what happens underneath: a CRDT sync engine (Yjs) that keeps every board converged across simultaneous editors, offline edits, and server restarts.

Multiple users can sketch freehand, drop shapes and text on an infinite canvas, see each other's cursors live, and reload later — everything merges.

Sync Yjs CRDT over a custom binary WebSocket protocol
Canvas React + Konva (react-konva)
Relay Node + ws — dumb update relay + per-board authoritative room doc
Offline IndexedDB cache; state-vector diff merge on reconnect
Persistence Postgres snapshotting with explicit compaction strategy

Features

  • Infinite canvas — freehand pen, rectangles, ellipses, arrows, and text; pan/zoom, multi-select, z-ordering, and delete via keyboard or panel.
  • Full-spectrum color control — preset swatches plus an HSV wheel picker (hue/saturation disc, brightness slider, hex input). Picks are gated behind an explicit OK, and each tool keeps its own recently used palette that persists across reloads.
  • Live collaboration — remote cursors, awareness-driven presence list, and advisory soft-lock hints; distinct users per board are capacity-capped while extra tabs of the same person still count once.
  • Scoped undo/redo — your undo stack never reverts a peer's concurrent edit.
  • Offline-first — edits made disconnected are cached in IndexedDB and merge through a state-vector diff on reconnect.
  • Durable boards — snapshot restore-on-load from Postgres when configured; memory-only otherwise.
  • Export & niceties — one-click PNG/SVG export, shareable board URLs, keyboard shortcuts (? shows the cheatsheet).

Quick start

pnpm install
pnpm build:shared
pnpm dev               # client on :5173 + relay on :3001, together

or run them individually:

pnpm dev:server        # relay on :3001 (memory-only without DATABASE_URL)
pnpm dev:client        # Vite on :5173, proxies /ws to the relay

Open http://localhost:5173 and you land on the marketing page — it includes a live, drawable demo with two scripted peers. Create a room (or any board URL) drops you straight into the canvas: each board id lives in the URL hash (/#abc123) — share it to collaborate. The brand chip in the board's top-left returns to the landing page.

Optional durable persistence:

cp apps/server/.env.example apps/server/.env   # set DATABASE_URL

The server logs Persistence : postgres on boot when configured; otherwise it runs memory-only. Check any board's storage stats at /stats?board=<id>.

Architecture

flowchart LR
    subgraph Browsers
        A["Browser A\nReact/Konva + Y.Doc"]
        B["Browser B\nReact/Konva + Y.Doc"]
    end
    subgraph Relay["Node WS Relay"]
        R["Room doc (per board)"]
        AW["Awareness relay\ncursors · selections · soft locks"]
        S["Snapshot scheduler\ndebounce 5s · ≥100 updates · ≥256KB"]
    end
    P[("Postgres\nboards.snapshot")]
    I[("IndexedDB")]

    A <-- "binary updates / awareness" --> R
    B <-- "binary updates / awareness" --> R
    R --- AW
    R --- S
    S -->|"merged full-state snapshot"| P
    P -.->|"restore-on-load"| R
    A -.->|"offline edits cached"| I
Loading

Full detail — wire protocol table, join handshake, compaction policy — lives in docs/architecture.md.

What the CRDT actually guarantees here

The point of this project is that conflict resolution is structural, not procedural. There is no lock server, no merge UI, no "last saved wins" dialog. Each of these is exercised by executable tests (see next section):

  • Concurrent edits converge deterministically. Two users resizing the same rectangle produce identical final geometry on every replica. Fields merge last-writer-wins ordered by Yjs logical clock + client id — never wall-clock, so clock skew is irrelevant. A field may come from writer A while its sibling comes from writer B; both outcomes are valid and all peers agree. (CS-1)
  • Delete wins races against edits. Deleting tombstones the entry: drag frames generated mid-deletion cannot resurrect it, even when replayed after the fact. (CS-2)
  • Offline work is never lost. Edits made disconnected ride out through a state-vector diff on reconnect and merge with everything that happened in between — including other people editing the same shapes. (CS-3)
  • No corruption from overlapping input. Distinct strokes are distinct keys; concurrent writes to one stroke resolve to a single intact version rather than an interleaved mess. (CS-4)
  • Rendering stays consistent under metadata races. Concurrent z-index reorders converge to one mapping everywhere; paint order derives from that identical mapping via stable sort. (CS-5)
  • Undo respects collaboration boundaries. Undo/redo is scoped by transaction origin: your undo stack physically cannot revert a peer's concurrent edit — inverse ops propagate like any other edit. (M9)
  • Nothing blocks. The soft-lock indicator ("Alice is editing…", amber handles) is advisory UI only. CRDT convergence makes hard locks unnecessary for correctness; they'd only add latency.

Scenario-by-scenario mechanics and observed outcomes: docs/conflicts.md.

Testing

pnpm test:crdt     # CS-1…CS-5 conflict scenarios against the real relay (spawns server)
pnpm test:undo     # scoped-UndoManager guarantee checks against real client modules

Both harnesses exit non-zero on failure and print per-check PASS/FAIL lines.

Repository layout

apps/
  client/            # React + Konva whiteboard
    src/crdt/        #   ydoc, WS provider, awareness, IndexedDB, undo scoping
    src/canvas/      #   Board, shapes, cursors, overlays
    scripts/         #   undo-smoke.ts
  server/            # Node WS relay + Postgres persistence
    src/             #   index.ts (relay), db.ts (snapshot store)
    dist/            # built output (harness spawns this)
    scripts/         #   conflict-scenarios.ts (CS-1…CS-5)
packages/
  shared/            # shape schema + protocol types shared by both apps
docs/                # architecture, conflicts, requirements, PRD, status

Documentation

Status

Milestones M0–M11 complete; M12 polish done except deployment (intentionally deferred). Post-milestone UI work added the HSV color wheel picker with persistent recently-used colors. Remaining stretch ideas live in docs/status.md.

About

Real-time collaborative whiteboard built on a Yjs CRDT sync engine, infinite canvas (React + Konva), live cursors, offline editing with state-vector merge, and Postgres snapshotting over a custom binary WebSocket protocol.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages