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.
- client
- service
- database or storage
- request
- Each browser sends its edits to the sync gateway over a WebSocket.
- The gateway appends every edit to the room's log on the Raft leader, and sends it to everyone else in the room.
- 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.
- client
- service
- database or storage
- request
- The tutor draws, and it shows on their screen immediately.
- The edit goes to the gateway as a Yjs update.
- The gateway sends it to the student, whose board merges it.
- At the same time, the gateway appends it to the room's log on the Raft leader.
- 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.
| Measure | Result |
|---|---|
| Test runs | 100 |
| Confirmed writes lost | 0 |
| Leader failover time | ~1.4s, mostly the election timeout |
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 + Raft | Durable Objects | |
|---|---|---|
| Replication | Mine, with Raft | Handled by Cloudflare |
| Room coordination | The gateway routes rooms | One object per room, built in |
| Measured | 100 editors, p95 < 90ms | 500 connections, 75ms p95 |
Key decisions
| Decision | Instead of | Why |
|---|---|---|
| CRDTs (Yjs) for sync | Operational transformation | No central transform logic; reconnecting is simple |
| Raft for storage | One database node or async replication | No confirmed edit lost when any one node fails |
| Confirm after majority commit | Confirm when the leader writes | A leader crash can't lose a confirmed edit |
| 1.4s election timeout | A shorter timeout | Fewer needless elections; the CRDT covers the gap |
| Snapshots and log compaction | Replaying full history | Rooms open quickly |
Results
- concurrent editors
- 100, p95 < 90ms
- leader failover
- ~1.4s
- lost commits
- 0 in 100 node-kill tests
- edge prototype
- 500 connections, 75ms p95