Skip to content

Collaborative Whiteboard

Sceneledger, a shared online whiteboard I built for small teams' live design reviews: a React drawing app, a FastAPI server with accounts and live WebSocket sessions, and a PostgreSQL database. This page runs its editor and every order two people's simultaneous edits can reach each screen.

Original built with React · TypeScript · Python · FastAPI · WebSockets · PostgreSQL · Docker Compose

What I found

6 of the 8

possible orderings of two simultaneous recolours leave a screen showing a colour the database does not have, when each client applies the server’s echoes as they arrive, as Sceneledger does now.

0 of the 8

orderings leave a mismatch when each client applies the echoes in sequence order, the numbering the server gives each stored edit. The client already records those numbers; it does not order by them.

2

undo cases that wipe out the other person’s newer edit even when every message arrives in order. Sceneledger’s own notes document both.

Try it

Two people recolour the same note at once. The server stores both edits one at a time, numbers them in the order it stored them, and sends each one back to both screens as an echo. It sends each echo after releasing its lock on the board, so the two sends can overtake each other and each screen can receive the echoes in either order: eight possible orderings in all.

Each client applies the server’s echoes

Each row is one possible ordering, counted once. Stored first is whose edit the server saved first, so the database, the saved copy, ends with the other. A receives and B receives give the order the two echoes, seq 1 and seq 2 by their sequence numbers, reached each screen. Red marks a screen showing a colour the database does not have: 6 of the 8 orderings with the echoes applied as they arrive.

Stored firstA receivesB receivesDatabaseA showsB shows
A’s roseseq 1 then 2seq 1 then 2blueblueblue
A’s roseseq 1 then 2seq 2 then 1bluebluerose, not the database
A’s roseseq 2 then 1seq 1 then 2bluerose, not the databaseblue
A’s roseseq 2 then 1seq 2 then 1bluerose, not the databaserose, not the database
B’s blueseq 1 then 2seq 1 then 2roseroserose
B’s blueseq 1 then 2seq 2 then 1roseroseblue, not the database
B’s blueseq 2 then 1seq 1 then 2roseblue, not the databaserose
B’s blueseq 2 then 1seq 2 then 1roseblue, not the databaseblue, not the database

Applying the echoes in sequence order fixes these eight, but not everything a shared board needs: in the two undo cases below, someone still loses an edit even when every message arrives in order.

Undo over a newer edit

  1. A recolours the white note rose.
  2. B recolours it blue.
  3. A presses undo.

The note goes back to white: A's undo restores the colour it replaced, and B's blue is gone.

Undo of a shape someone moved

  1. A adds the note.
  2. B moves it across the board.
  3. A presses undo.

The note is deleted, and B’s move with it.

Try the editor

An adaptation of Sceneledger's drawing editor, running in this tab alone: there is no second person or server here, and the board is saved in this browser. Click or tap an object, or pick it in the list, then drag it to move it or drag a corner to resize it; pick a colour to recolour it. Each object moves on its own: the editor has no groups, so a caption stays put when its shape moves. On a keyboard, arrow keys nudge the selected object, ten units with Shift, and letting go records the nudges as one edit; Delete removes it, Control Z undoes and Control Y redoes.

This tab onlyOpening board8 objects
The editable object list and properties are available below.

No object selected

960 x 600No selection
Under the hood: operation log, export and import

Operation log

0 commands0 undo / 0 redo

No edits yet

Contents

Whose edit does each screen show?

A shared whiteboard has to answer one question again and again: when two people change the same thing at once, what does each of them see afterwards, and does it match what was saved? Sceneledger, the whiteboard I built, answers it with one server, one lock per board so the server stores one edit at a time, and a sequence number for every stored edit. Each client, a person's browser, applies its own edit at once and sends it; the server stores and numbers it, then sends it back to everyone as an echo, which each client applies in turn.

Every order two edits can take

A recolours a note rose while B recolours it blue. The server stores one first and numbers them 1 and 2, so the database ends with whichever it stored second. Each echo then goes out separately, after the lock is released, and each client can receive the two in either order: two orders at the server, two at each client, 8 possible orderings in all. Because each client applies the echoes as they arrive, the last one to arrive wins on that screen, and in six of the 8 someone ends up looking at a colour the database does not have; in two, both people do.

The client already records each echo's sequence number, but does not order by it. Holding echoes and applying them in that order makes all 8 agree: that is the table's second setting, a fix this page models, not one the Sceneledger client at the linked commit makes. It fixes ordering, not intent: undo is a second problem. Undo sends an inverse operation, an ordinary edit that reverses the client's own last one. In order, with every message delivered before the next action, A recolours the note, B recolours it after, and A undoes; A's undo puts back the colour it replaced, and B's newer colour is gone. If A adds a note, B moves it and A undoes the add, the note is deleted and B's move with it. The source documents both.

What this page is not

There is no server, account or second person here: the table works through a model of the protocol in every order the two edits can take, and the editor runs in this tab alone. The opening board is a made-up workflow of 8 objects, not anyone's work.

For engineers

Where the source leaves room for disagreement, how the editor keeps its history, where the code comes from, its limits in detail, and how to run the tests.

Where the source leaves room

The source's notes are candid that its design does not guarantee convergence, every screen ending on the database's state: the broadcast happens after the lock is released, clients do not enforce the sequence numbers, and its own two-client test covers an ordinary add, not two people editing the same field. The table runs that case through every order it can take.

How the editor keeps history

A scene, not pixels. The editor keeps a list of shapes on a 960 by 600 board and changes it only through three operations: add, update and delete. Each edit stores its inverse: an add is undone by deleting it, an update by restoring only the fields it changed, a delete by re-adding the shape at its old position in the list, so the overlap order survives undo. A clear is one command of many deletes, and a drag, a resize or a run of arrow-key nudges is one edit when it ends, however many steps it took.

Hit testing follows the drawing. The selected shape is tested first, then the rest from the top down; rectangles and text by their boxes, ellipses by their equation, lines by distance to the segment. The selected shape is also drawn last, so what is drawn on top is what a press picks.

History you can replay. The board, its history and the undo and redo stacks save to this browser and export as a versioned trace. Import replays every command through the same engine and refuses a trace whose declared final board does not follow from its history. After a saved board fails to load, edits stay in memory and the unreadable copy is kept, until a reset or import replaces it.

Where this comes from

Sceneledger is my full-stack whiteboard: a React canvas client, FastAPI HTTP and WebSocket sessions with JWT accounts and board membership, and PostgreSQL storage for shapes. The table models its client operations, the client's echo handling and the server's op path; the operations pass the source's own client tests. The editor is adapted from the same client under its Apache 2.0 licence, with the changes listed.

Limits in detail

The table enumerates the protocol's orders; it does not run sockets. The editor does not sync between tabs, and the last tab to save wins. Boards hold up to 200 objects and 1,000 commands, and traces up to 1 MB. The editor has no groups and no connectors, as its object list shows: each object moves on its own.

Reproduce it

Switch the table to sequence order and watch the red go; then draw, drag, nudge with the arrow keys and undo in the editor, and export and import its trace under the hood. The protocol model, the editor's engine and their tests are in the code for this page. From the site's Next.js app:

npx vitest run src/lib/projects/collaborative-whiteboard src/components/projects/collaborative-whiteboard