← Projects

TutorBoard

A real-time whiteboard for tutoring sessions that keeps every saved edit even when a server dies.

0
writes lost in 100 node kills
1.4s
leader failover
100
concurrent editors, p95 under 90ms

Solo project, for SFU CS peer tutoring

Go, Raft, Yjs, WebSockets, TypeScript, Cloudflare Durable Objects

What it is

As a CS peer tutor at SFU, I wanted a shared whiteboard where a tutor and students can draw, write code snippets and annotate together in real time, without losing work if a server dies. TutorBoard syncs edits between browsers with CRDTs over WebSockets, and stores each board in a three-node Raft cluster I wrote in Go.

A collaborative whiteboard has two separate problems. Live sync: when two people draw at once, every screen ends up showing the same picture, quickly, with no conflict dialogs. Durability: once an edit is confirmed, it survives a server crash. TutorBoard uses a different tool for each, CRDTs for sync and Raft for durability.

TutorBoard architecture
TutorBoard architectureA tutor and students each hold a Yjs document in the browser and send updates over WebSockets to a sync gateway written in Go. The gateway fans each update out to everyone else in the room and appends it to the room's log in a three-node Raft cluster, also written in Go. The leader replicates the entry to both followers, and it counts as committed once two of the three nodes have it.BrowsersRaft cluster (Go)TutorYjs docStudent 1Yjs docStudent 2Yjs docSync gatewayGo · room fan-outLeaderroom logsFollowerFollowerWebSocket1append2replicate3fans each update outto the rest of the roomcommitted once 2 of 3 have it
  • client
  • service
  • database or storage
  • request
  1. Each browser sends its edits to the sync gateway over a WebSocket.
  2. The gateway appends every edit to the room's log on the Raft leader, and sends it to everyone else in the room.
  3. The leader replicates the entry to both followers. It's committed once two of the three nodes have it.

Live sync with CRDTs

Each browser holds a Yjs document, a CRDT (conflict-free replicated data type) describing the board: shapes, strokes, text and sticky notes. Every local edit is applied right away and becomes a small binary update sent to the gateway, which passes it to everyone else in the room. CRDT updates can arrive in any order, or twice, and every copy still ends up in the same state, so there's no locking and no central decision about who wins.

One edit, end to end
One edit, end to endThe tutor draws an arrow, and it shows up on their own screen right away. The Yjs update goes to the gateway, which sends it to the student and appends it to the Raft leader at the same time. The student merges the update. Once a majority of Raft nodes has the entry, the leader confirms it and the gateway acknowledges the edit as saved.TutorSync gatewayStudentRaft leaderdraw (shows instantly)1Yjs update2fan out3merge, convergeappend to room log4committed (2 of 3)saved5
  • client
  • service
  • database or storage
  • request
  1. The tutor draws, and it shows on their screen immediately.
  2. The edit goes to the gateway as a Yjs update.
  3. The gateway sends it to the student, whose board merges it.
  4. At the same time, the gateway appends it to the room's log on the Raft leader.
  5. Once two of three nodes have it, the gateway tells the tutor the edit is saved.

I chose CRDTs over operational transformation (OT), which needs a central server to transform concurrent edits against each other and is hard to get right. CRDTs build convergence into the data structure, which also makes reconnecting simple: a client exchanges state vectors with the server and receives only what it missed.

With 100 people editing one room, an edit reaches the other screens in under 90ms at p95 within the same region.

Durability with Raft

Each room's history is an append-only log of Yjs updates plus periodic snapshots, stored in a document store I wrote in Go and replicated across three nodes with Raft.

How Raft works, briefly: one node is the leader, and all writes go to it. The leader appends each entry to its log and copies it to the followers. Once a majority (two of three) have it on disk, the entry is committed. If the leader stops sending heartbeats, a follower times out and calls an election, and Raft's rules guarantee the new leader already has every committed entry. An edit is confirmed only after it's committed, so any single node can die, including the leader, without losing a confirmed edit.

To check that, I wrote a test harness that keeps clients editing while it kills random nodes, often the leader, mid-write. After recovery it checks that every confirmed write is still there.

MeasureResult
Test runs100
Confirmed writes lost0
Leader failover time~1.4s, mostly the election timeout
Results of the node-kill tests.

Failover time is a trade-off. A shorter election timeout fails over faster, but causes needless elections on a slow network. At 1.4 seconds the cluster stays stable, and clients only see a brief reconnect, while their local CRDT keeps working through it.

Whiteboards produce many tiny updates, so each room's log is compacted into a snapshot from time to time. Opening a room loads the latest snapshot plus the updates after it.

An edge prototype

I also built the sync layer on Cloudflare Durable Objects, with one object per room holding that room's WebSocket connections and state, running close to users. Comparing the two was the point: writing Raft myself taught me the failure cases, and the Durable Objects version showed how much a good managed primitive removes.

Go gateway + RaftDurable Objects
ReplicationMine, with RaftHandled by Cloudflare
Room coordinationThe gateway routes roomsOne object per room, built in
Measured100 editors, p95 < 90ms500 connections, 75ms p95
Self-hosted Raft vs Cloudflare Durable Objects, both measured within one region.

Key decisions

DecisionInstead ofWhy
CRDTs (Yjs) for syncOperational transformationNo central transform logic; reconnecting is simple
Raft for storageOne database node or async replicationNo confirmed edit lost when any one node fails
Confirm after majority commitConfirm when the leader writesA leader crash can't lose a confirmed edit
1.4s election timeoutA shorter timeoutFewer needless elections; the CRDT covers the gap
Snapshots and log compactionReplaying full historyRooms open quickly
The main choices, and what they replaced.

Results

concurrent editors
100, p95 < 90ms
leader failover
~1.4s
lost commits
0 in 100 node-kill tests
edge prototype
500 connections, 75ms p95