Multi-user, infinite canvas whiteboard slapped together by Claude 5.5
  • JavaScript 84.5%
  • CSS 8.2%
  • HTML 6.8%
  • Dockerfile 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Adam Macumber 640c3f543b feat: Wrap resized text boxes; add a text size menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:07:02 -04:00
lib feat: Wrap resized text boxes; add a text size menu. 2026-10-03 13:07:02 -04:00
public feat: Wrap resized text boxes; add a text size menu. 2026-10-03 13:07:02 -04:00
test feat: Wrap resized text boxes; add a text size menu. 2026-10-03 13:07:02 -04:00
.dockerignore feat: Dark mode. 2026-10-02 11:38:23 -04:00
.env.example feat: Dark mode. 2026-10-02 11:38:23 -04:00
.gitignore feat: Dark mode. 2026-10-02 11:38:23 -04:00
compose.yml feat: compose for traefik deployment. 2026-10-02 11:38:23 -04:00
Dockerfile feat: Dark mode. 2026-10-02 11:38:23 -04:00
package-lock.json Initial implementation 2026-10-02 09:32:26 -04:00
package.json Initial implementation 2026-10-02 09:32:26 -04:00
README.md feat: Wrap resized text boxes; add a text size menu. 2026-10-03 13:07:02 -04:00
server.js feat: Multiple boards. 2026-10-02 11:38:12 -04:00

Whiteboard

A multi-user infinite whiteboard that runs in the browser. Create as many boards as you like; on each one, draw, write text and paste or upload images together in real time, and zoom in or out without limit.

npm install
npm start            # http://localhost:3000

The home page lists every board, with when it was last edited and who's on it now. You can create, rename and delete boards there. Each board has its own URL (/b/<id>): share it with others on your network (HOST=0.0.0.0 npm start) and everyone on that link edits the same board.

The address bar also keeps track of where you are on the board (/b/<id>#x=…), so copying it, or using the link button next to the zoom controls, shares your exact view. Whoever opens it lands on the same spot at the same zoom; if their window is smaller than yours, it zooms out just enough to show everything you could see. A board link without a view opens where you last left that board.

Boards are saved to disk as they're edited, within half a second of each change:

data/
  boards.json        index of boards: names, created/edited times
  boards/<id>.json   one file per board, holding its elements
  uploads/           images, shared by all boards (content-addressed)

Every write goes to a temp file first and is then renamed into place, so a crash can't leave a half-written board. A board file that's missing from the index is recovered at startup. A data/board.json from the earlier single-board version is migrated into a board called "Whiteboard".

Variable Default Purpose
PORT 3000 HTTP / WebSocket port
HOST all Interface to listen on
DATA_DIR ./data Where the board and images are kept

Deploying with Docker and Traefik

compose.yml runs the whiteboard behind an existing Traefik instance, served over HTTPS with a Let's Encrypt certificate. It assumes Traefik:

  • uses the Docker provider,
  • is attached to an external Docker network (default traefik), and
  • defines a Let's Encrypt certificate resolver named letsencrypt.
cp .env.example .env    # set WHITEBOARD_HOST, and the network/entrypoint if yours differ
docker compose up -d --build
Variable Default Purpose
WHITEBOARD_HOST whiteboard.localhost Hostname Traefik routes to the app
TRAEFIK_NETWORK traefik External network shared with Traefik
TRAEFIK_ENTRYPOINT websecure Traefik entrypoint (HTTPS)

Boards and uploads are kept in the whiteboard-data volume. No port is published on the host: all traffic goes through Traefik, and the realtime WebSocket connection works through it with no extra config. The app has no login, so if it's reachable from the internet, add a Traefik auth middleware to the router (for example basicauth or forwardauth).

Using it

Action How
Draw Pen tool (P)
Erase Eraser tool (E) removes whatever it touches
Text Text tool (T), click to place; Esc or click away to finish
Images Image button (I), paste (Ctrl+V), or drag files onto the board
Select / move / resize Select tool (V); drag the corner handle to resize, Delete to remove, double-click text to edit
Wrap text Drag a text box's corner handle: the words rewrap to the new width; the text size stays the same
Text size The A menu in the toolbar sets the size of new text, or of the selected text
Recolor Select an element, then pick a color
Pan Pan tool (H), hold Space, middle/right drag, two-finger scroll, or drag empty space with Select
Zoom Mouse wheel, trackpad pinch, Ctrl+scroll, + / -, two-finger pinch on touch
Back to origin Crosshair button or 0
Rename the board Click its name (top left), or the pencil on the home page
Back to all boards Grid button (top left)
Light / dark mode Theme button (top right, or on the home page): System → Light → Dark
Undo / redo Ctrl+Z / Ctrl+Shift+Z (your own changes)
Find a collaborator Click their avatar (top right) to jump to their view
Share your exact view Copy the address bar, or the link button (top left)
Rename yourself Click your own avatar

How it works

Server (Node + ws):

  • server.js: HTTP routes. It serves the pages, accepts image uploads (PNG/JPEG/GIF/WebP, ≤ 20 MB), and provides the boards API: GET/POST /api/boards and GET/PATCH/DELETE /api/boards/:id.
  • lib/sync.js: one realtime room per board over /ws?board=<id>. It relays board operations: add, update, append (points added to an in-progress stroke) and delete, plus cursor and view presence. The server holds the authoritative state and validates every operation (lib/validate.js). New clients receive a full snapshot. Concurrent edits to the same element resolve last-writer-wins, which is plenty for ~10 people sketching together.
  • lib/boards.js: storage. A board is loaded into memory when its first user joins, and saved and unloaded after the last one leaves.

Client (public/, plain ES modules, no build step): a single <canvas> redrawn on demand. Edits apply locally right away and are sent to the server. Live strokes stream in 40 ms batches, so other users see you draw as it happens.

Dark mode

Each viewer picks their own theme (saved in their browser; "System" follows the OS setting). Board colors are shared, so they're always stored as drawn in light mode. In dark mode the canvas shows dark inks with their lightness mirrored (black ink appears near-white, navy appears light blue). Hue and saturation are kept, colors that are already light are left alone, and images are never altered (darkInk in public/theme.js). Everyone sees readable ink whichever theme they use.

Unbounded zoom

Ordinary 64-bit floats run out of precision after zooming in about 2^50×, at which point drawings start to jitter and snap. To avoid that, world positions are arbitrary-precision BigInt fixed-point numbers (value × 2^-precision). The zoom level is stored as a log2 exponent, and the camera's precision grows as you zoom in (public/coords.js).

Every element stores its origin in that format, along with the zoom level it was created at. Its own geometry is kept in ordinary floats relative to that origin. Rendering subtracts the camera position from the origin exactly in BigInt, and only converts the small screen-space result to a float. So a sketch drawn at 10^3000× magnification is just as crisp as one at 100%, and the tests check sub-pixel accuracy after zooming in 2^10000×. The dot grid uses power-of-two spacings that cross-fade, so you can always see the zoom moving.

Tests

npm test

Covers the coordinate math (deep zoom in and out, panning, moving) and the server: the boards API, isolation between boards, relay and snapshots, validation, presence, uploads, saving while editing, unloading and reloading, restarts, and migration.

Limitations

  • No authentication: anyone who can reach the server can see, edit, rename and delete every board. Put it behind a reverse proxy with auth for anything public.
  • Deleting a board is permanent. Its uploaded images stay in data/uploads/, since other boards may use them too.
  • Undo covers your own changes and is lost on page reload.
  • Edits made while disconnected are discarded; the UI only allows panning and zooming until the connection returns.