Jagger PapersTechnical notes & papers
Jagger Papers

Systems / Research Notes

LiveBoard: A Distributed Realtime Whiteboard With Durable Collaboration State

A collaborative whiteboard with shared undo, live previews, and durable edits.

TypeScriptReactFastAPIPostgreSQLRedisWebSocketsDockerDistributedRealtime CollaborationUndo RedoSVG EditingAccess ControlRate LimitingTransient OperationsOperational State

Abstract

LiveBoard is a collaborative whiteboard designed to combine responsive editing with shared undo and durable recovery. It separates transient gesture previews from committed operations, which the backend serializes in PostgreSQL while deriving inverses from the affected state. Redis distributes room events, presence, and invalidation across backend replicas. Optimistic edits are reconciled using operation IDs, and revision gaps trigger snapshot recovery. This architecture supports live interaction without storing every pointer movement, while keeping history tied to committed state. Its centralized revision order provides shared undo but leaves offline merging and per-character collaborative text outside the current model.

LiveBoard editor surface. The canvas viewport, toolbar state, shape transforms, presence cursors, and server-backed revision stream all meet in this view.

A Shared Canvas

LiveBoard is a collaborative whiteboard with shared undo and durable history. A drag is visible to collaborators before its endpoint is saved; undo, refresh, and reconnect must still return everyone to a consistent document.

  1. 01Local gesture
  2. 02Preview / commit
  3. 03Validate & lock
  4. 04Save revision
  5. 05Fan out / recover
LiveBoard at a glance. Validated operations connect client interaction to committed state and collaboration.
BrowserCaddyReplica AReplica BReplica CPostgreSQLRedisdurable statecoordination
Runtime boundary. HTTP and WebSocket traffic can land on any backend replica. PostgreSQL remains authoritative for durable state; Redis coordinates events that must cross process boundaries.

It separates transient motion from committed operations. React owns immediate interaction state, PostgreSQL holds the drawing and history, and the backend validates writes. Redis distributes room events and coordination across replicas. Previews keep a gesture responsive without filling revision history with pointer samples.

The Document Model

A canvas is an ordered JSON document with stable shape IDs, geometry, style, and optional background color. Array order determines stacking. Operations address IDs, so a shape can be patched or reordered without sending the entire drawing.

select units
  -> unit can be one unlocked shape or one already-grouped object
  -> Group appends new parent group id to every selected unit
  -> selection reconciles to the new top group
  -> move/scale/rotate operate on the group as one unit

Ungroup
  -> remove only the active/top group id
  -> preserve child group ids underneath
Nested grouping flow. Groups are stored directly on member shapes. Nested grouping is a stack operation, not a separate tree of group nodes.

Keeping shapes in one document avoids per-kind relational schemas and lets the backend apply an operation against one state. Shape-specific validation still restricts supported fields and patches before mutation. The same validated operation feeds storage, undo history, and realtime delivery.

Editor Geometry

The SVG viewBox defines the viewport’s document-space region. Pointer input is converted through its scale and origin before editing; zoom and pan remain view state rather than changes to saved geometry.

idle
  -> background pointer down with select tool: box-selecting
  -> drawing tool pointer down: drawing draft shape
  -> selected shape pointer down: moving
  -> resize handle pointer down: resizing
  -> rotation handle pointer down: rotating
  -> middle button down: panning local viewport

pointer up
  -> local preview becomes one durable operation when the gesture changed shared canvas state
Pointer interaction states. The frontend keeps local interaction state explicit so it can decide which gestures are local-only, transient previews, or durable history entries.

Group translation preserves relative placement, and group rotation uses the selection center. Handles, bounds, previews, and the committed transform share those calculations. Otherwise a gesture can look correct locally while publishing a different position or pivot to collaborators.

Drag, resize, and rotation publish intermediate previews and commit on completion. Text and color changes have their own commit points. Geometry determines the operation’s values, while the interaction lifecycle determines when they become durable.

An Edit and Its Undo

The backend derives each inverse from the saved state it actually changes. A client’s view may omit an intervening edit, so a client-supplied inverse could restore the wrong value. Shared undo needs the prior committed state, not the sender’s private account of it.

draw shape              -> create_shape
drag one shape          -> preview_op update_shape, then durable update_shape
drag group or selection -> preview_op batch, then durable batch
edit text content       -> local textarea draft, then durable update_shape
change style            -> update_shape or batch
paint background        -> update_canvas
bring forward/back      -> reorder_shape
group/ungroup           -> batch of groupIds patches
undo/redo               -> server applies stored inverse/forward op
Feature to operation mapping. The editor has many features, but they collapse into a small operation vocabulary that the backend can validate, persist, invert, and broadcast.

PostgreSQL stores canvas state and revision alongside operation history; ownership, membership, sessions, and folders remain relational. A row lock serializes canvas writes. Validation, mutation, inverse computation, and history update occur in one transaction, keeping the revision and undo record aligned.

Undo and redo are shared mutations processed through that server state and broadcast to all editors. The stored document also provides the recovery snapshot for clients missing intervening events.

Batches preserve gesture-level undo. Their inverse reverses both the constituent operations and their order, so dependencies such as creation followed by modification are undone safely. A multi-object transform can remain one history step without discarding its internal ordering.

opr−1=invert(Sr,opr)op_r^{-1} = invert(S_r, op_r)
Inverse derivation. S_r is the saved canvas state before operation op_r. invert computes its inverse, the operation that restores the affected state for undo.

Previews, Commits, and Recovery

Throttled previews cover drag, resize, and rotation; the final operation takes the durable transaction path. Color-picker exploration stays local until commit. Previews do not advance revision history and can be discarded during recovery.

Canvas editor. The editor sends durable operations for committed changes and preview operations during high-frequency drag, resize, and rotation gestures.
client A pointer move
  -> preview_op(batch update_shape...)
  -> room fanout
  -> client B applies transient preview

client A pointer up
  -> op(batch update_shape..., undoable=true)
  -> backend validates + locks + persists
  -> op_applied(revision+1, history_status)
  -> all clients reconcile state
WebSocket message flow. The same operation shape is used for previews and commits, but only committed operations enter PostgreSQL history.
client current revision = 17
receives op_applied revision = 19
  -> expected 18, observed 19
  -> pause operation application
  -> GET /api/canvases/:canvasId
  -> replace local canvas state, revision, history
  -> resume from durable PostgreSQL snapshot
Revision-gap recovery. Redis Pub/Sub is best-effort. Durable correctness comes from revision numbers plus snapshot refresh when a client detects that it missed an operation.
Rejected writerate_limitedWriter snapshotpreview_resetPeer refreshDurable canvassender replaces local optimismpeers discard transient preview
Rate-limit recovery. Rejected writes are never persisted or fanned out. The sender and peers both return to the last durable canvas so transient previews cannot become visible drift.

The editor fetches state through HTTP, then receives an authoritative snapshot, revision, presence, and undo/redo availability over WebSocket. Previews affect the temporary display; committed events advance its document state.

The sender applies a final operation optimistically. Operation IDs identify its broadcast echo and prevent duplicate application. A rejected write or replacement snapshot clears the optimistic state.

Committed revision gaps trigger snapshot recovery instead of assuming the client can reconstruct missing edits. Reconnects and rate-limit interruptions use that path too. Because the snapshot restores durable state, it does not need to reproduce intermediate pointer motion.

Multiple Servers and Presence

With multiple backend replicas, committed state alone does not deliver an edit to every socket. Redis Pub/Sub distributes room events to the processes serving those connections, and each forwards locally. Presence, invalidation, and shared rate-limit counters use Redis too; PostgreSQL remains the source for recovering missed committed events.

Client AReplica APostgreSQLRedis Pub/SubReplica BClient BClient Ccommit firstpublish after commit
Cross-replica fanout. A room is logical, not process-local. A replica commits durable operations to PostgreSQL, then uses Redis Pub/Sub so every other replica can forward the event to its local sockets.
socket accepted
  -> add connection id to canvas set
  -> write connection record with TTL
  -> if previous user connection count was 0: presence_join

socket closed
  -> remove connection id
  -> wait briefly
  -> if user connection count is still 0: presence_leave
Presence transition. The collaborator list is derived from Redis presence in scaled mode and from local sockets in single-process fallback mode.

Presence counts connections per user rather than treating each tab as a separate collaborator. Connection records expire by TTL. Join and leave events occur on the transitions between zero and nonzero connections, with a short leave delay to avoid refresh flicker. Expiration also repairs presence after a process exits without disconnect cleanup.

Durable-write rejection needs room-wide cleanup too. The writer receives a saved snapshot and resynchronizes; peers receive a preview reset so rejected motion does not remain visible. Shared write limits protect revision and history growth across replicas, while cursor and preview traffic have higher allowances. Recovery therefore handles failed commits as well as missed broadcasts.

Access Control

Ownership or current membership authorizes both HTTP requests and socket messages, using an httpOnly session cookie. Authorization is enforced on canvas access and edits, independently of what the dashboard displays.

Sharing modal. The share modal exposes the membership set and lets the owner add or remove access without leaving the editor.
owner opens share modal
  -> GET /api/canvases/:id/members
  -> owner enters username or email
  -> POST /api/canvases/:id/invite
  -> backend verifies owner and target user
  -> INSERT canvas_members
  -> modal updates member list
  -> invited user sees canvas in Shared with You
Invite flow. Sharing is stored as membership rows. Folders remain owner-local and do not affect whether the invited user can open the canvas.
owner removes member
  -> DELETE /api/canvases/:id/members/:userId
  -> database membership row deleted
  -> Redis invalidation reaches every backend replica
  -> each room manager finds matching local sockets
  -> access_removed message
  -> socket close
  -> removed client stops receiving updates
Removal path. Access changes affect both future authorization and currently connected editors.

Removing a member or deleting the canvas also invalidates established sockets. The backend persists the change and distributes invalidation to the replicas holding those connections. Handshake-time authorization alone would leave a removed collaborator in the live room.

The Dashboard

The saved document also belongs to a workspace. Owners organize canvases in a folder tree; collaborators find them among shared canvases. Folder placement and membership are separate, so reorganizing the workspace does not change access.

Dashboard file structure. The dashboard presents owned canvases as a mixed folder/canvas tree and shared canvases as a separate searchable section.
drag owned item
  -> hover center of folder row
     -> PATCH canvas folder or folder parent
     -> backend verifies ownership and cycle rules
     -> frontend refreshes owned tree placement

drag owned item
  -> hover insertion zone before/between/after siblings
     -> PATCH /api/dashboard/order with full mixed order
     -> backend rewrites sort_order for that parent
     -> frontend renders arbitrary sibling order
Dashboard drag/drop flow. The dashboard supports both parent changes and arbitrary sibling reordering. The backend stores order explicitly instead of inferring it from names or creation time.

Drag and drop persists both parent changes and sibling ordering, including mixed ordering of folders and canvases. Tree rails are derived from those sibling relationships. Deleting an owned subtree removes its resources, while removing a member leaves the document in place.

Validation and Tradeoffs

A group move followed by undo exercises geometry, previews, committed state, and history together. Repeating that sequence across replicas and interrupted connections checks whether recovery converges on the same saved revision, not only whether a gesture looks correct in one browser.

sticky sessions only
  -> rejected: correctness should not depend on load-balancer affinity

PostgreSQL LISTEN/NOTIFY for everything
  -> rejected: high-frequency preview/cursor traffic should not live in the durable database path

Redis Streams for all collaboration events
  -> rejected: durable operations are already recoverable by revision snapshot

Redis canvas-state cache
  -> rejected: adds invalidation around the most important state
Rejected alternatives. The chosen architecture is intentionally boring where correctness matters and fast where state is temporary.

LiveBoard serializes operations on the server. That supplies one revision order and one shared history, at the cost of leaving offline merging outside the current model. Per-character collaborative text, richer roles, image storage, and exports are also outside its current scope.

Shared history is the central constraint on collaboration here. Preview delivery can be lossy; committed state and inverses cannot disagree. Snapshot recovery lets the live protocol remain lightweight while the transaction path supplies a definite state to return to.