Build Excalidraw+

YESback to the verdict

0%0 of 16 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A collaborative whiteboard where the difficulty is entirely in concurrent editing of a shared document: reconciling simultaneous changes without a central authority deciding who loses, keeping cursors live without persisting them, and encrypting scenes end to end, which then forbids every server-side feature you would otherwise want.

estimated effort multi-daythe files for this build are in the project pack

CanvasA scene of independent elements, each separately versionedTransportWebSocket for live changes, HTTP for scene loadingReconciliationPer-element versioning with a deterministic tiebreakEphemeral stateCursors and selections broadcast but never storedEncryptionScene encrypted in the browser with a key in the URL fragment

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the socket server and builds the client.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. have readyfree

    Why Every claim about collaboration is untestable with a single client, and two tabs in one browser hide timing problems that two machines expose.

    Get it A second machine, or at minimum two different browsers with the network throttled differently.

    Verify Two clients can join one room and both see each other's changes

  3. have readyfree

    Why Delta transmission, snapshot cost and canvas performance all behave differently at five hundred elements, and a test scene of ten proves nothing.

    Get it Draw one, or generate a scene with several hundred varied elements including freehand paths, which are the expensive ones.

    Verify A scene with five hundred elements including freehand paths exists

  4. decidefree

    Why End-to-end encryption forbids server-side search, thumbnails, export and key recovery, and retrofitting it later means removing features people already use.

    Get it Decide, and write down what you are giving up in exchange. Both answers are defensible; discovering the trade halfway is not.

    Verify A written note states the decision and lists the features it forecloses

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- scenes(id, encrypted_blob, iv, version, updated_at) · the server sees ciphertext and nothing else
- elements(id, type, x, y, width, height, angle, points_json, style_json, version, version_nonce, updated_by, deleted)
- Elements live inside the encrypted scene. The server schema above is what a self-hosted unencrypted mode would use
- rooms(id, scene_id, created_at, last_active_at) · a live session, holding no content
- presence(room_id, client_id, name, colour, cursor, selection) · in memory only, never written to disk
- A deleted element is a tombstone with a version rather than a removed row, because a delete and a concurrent edit must reconcile.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/canvasEncrypted scene blobs and room records. Contains no readable content by design.
SOCKET_PORTrequired3001The live collaboration channel, separate from scene loading over HTTP.
PRESENCE_THROTTLE_MSrequired50How often cursor positions broadcast. Lower feels smoother and floods the channel.
PRESENCE_TIMEOUT_SECONDSrequired30When a silent client's cursor is removed. Clients vanish without disconnecting.
SNAPSHOT_DEBOUNCE_MSrequired2000How long after the last change a snapshot is written. Per-change does not scale.
MAX_SCENE_BYTESrequired10485760A cap, since the server cannot inspect an encrypted scene to judge whether it is reasonable.

The build, in order

  1. A canvas with versioned elements

    Drawing works locally and every element carries what reconciliation will need.

    1. A counter and a random tiebreak. Both are needed for a deterministic resolution.

      Files src/element.ts

    2. A removed row cannot reconcile against a concurrent edit.

      Files src/element.ts

    3. A reload mid-drawing must lose nothing.

      Files src/persist.ts

    done when · tick each as it passes
    watch out
    • Deleting an element by removing it from the scene makes a delete concurrent with an edit unresolvable, because the client that edited it simply reinserts it and the element comes back from the dead. Tombstone with a version, and let the normal reconciliation rules decide.
  2. Two people drawing at once

    Simultaneous edits from two clients resolve identically on both.

    1. Two elements in, one out, deterministic. Unit test with conflicting pairs.

      Files src/reconcile.tssrc/reconcile.test.ts

    2. Both clients must reach the same answer without consulting the server.

      Files src/reconcile.ts

    3. Whole-scene broadcast collapses at a few hundred elements.

      Files src/sync.ts

    done when · tick each as it passes
    watch out
    • Sending the entire scene on every change works beautifully with two elements and collapses at five hundred, saturating the connection and making the canvas lag behind the pointer. Send only what changed, and make reconciliation a pure function you can test with two conflicting element pairs directly.
  3. Cursors, without drowning the connection

    You can see where other people are, and it costs almost nothing.

    1. Its own channel, a fixed rate. Drawing must never queue behind pointer noise.

      Files src/presence.ts

    2. It is worthless a second later and it is a record of what people looked at.

      Files src/presence.ts

    3. Clients vanish without disconnecting. Ghost cursors accumulate otherwise.

      Files src/presence.ts

    done when · tick each as it passes
    watch out
    • Sending a cursor position on every pointer move puts hundreds of messages a second on the same channel as element changes, and the drawing updates end up queued behind pointer noise. Throttle presence, put it on its own channel, and drop it rather than buffering it when the connection is congested.
  4. Encrypted scenes, and living with the consequences

    The server stores scenes it cannot read, and you accept what that forbids.

    1. The server should never be able to read a scene, even accidentally.

      Files src/crypto.ts

    2. Fragments are not sent in requests. Verify that in server logs, do not assume it.

      Files src/share.ts

    3. Search, thumbnails, recovery. Decide each now rather than discovering it later.

      Files docs/encryption-tradeoffs.md

    done when · tick each as it passes
    watch out
    • End-to-end encryption removes server-side search, thumbnails, exports, moderation and any recovery of a lost key, and each of those tends to be discovered as a surprise halfway through a later feature. List them at the start: if the key is lost the scene is gone, and there is no reset.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more whiteboards.