Build Miro

KINDAreplaces $10/mosaves $120/yrback to the verdict

0%0 of 20 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 canvas that stays smooth with ten thousand objects because it only draws what is on screen, with sticky notes, shapes, connectors and freehand ink, and genuine concurrent editing built on a conflict-free replicated data type so two people typing at once converge rather than overwrite each other.

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

RuntimeTypeScript with Vite, rendering to a canvas elementCollaborationYjs, a conflict-free replicated data typeTransportA WebSocket relay that persists the documentPersistenceThe Yjs document state plus periodic snapshotsSpatial indexA quadtree over object bounds

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 relay and the build.

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

    Verify node --version prints v22 or higher

  2. have readyfree

    Why Every collaboration claim in Phases 3 and 4 needs two participants. A single browser with two tabs is a weaker test but is the minimum.

    Get it Two different browsers on one machine, or two machines on the same network. Two machines catch clock and latency issues that two tabs hide.

    Verify Two clients connect to the same board simultaneously

  3. decidefree

    Why It is the central architectural decision, and retrofitting it after building on last write wins means rewriting every mutation.

    Get it Read the Yjs documentation on shared types before Phase 3, not during it. Half an hour saves a rewrite. open ↗

    Verify You can explain why two concurrent text edits merge rather than one winning

  4. have readyfree

    Why Phase 1's performance work is meaningless without it, and the naive approach feels perfect until this fixture exists.

    Get it Write the generator in Phase 1 before optimising anything.

    Verify The generator produces a board that loads and renders

  5. have ready5 to 10 USD a month

    Why Only needed once other people use it. Everything before that runs locally.

    Get it Any small virtual server with a domain, or a machine at home behind a tunnel.

    Verify The relay is reachable over TLS

Data model

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

- The board is one Yjs document. These are the shared types inside it, not database tables.
- objects · a shared map of id to object: kind, x, y, w, h, rotation, z, style and content
- text · per-object shared text so two people editing the same sticky note merge rather than clobber
- awareness · ephemeral cursor position, selection and name per participant, never persisted
- Server side: boards(id, name, created_at), updates(board_id, seq, payload, at), snapshots(board_id, seq, state, at)
- The rule that keeps multiplayer honest: every change goes through the shared document, including your own local edits. There is no separate local path.

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
RELAY_URLrequiredwss://boards.example.comWhere clients connect. Must be a secure WebSocket in anything but local development.
PERSIST_DIRrequired./data/boardsWhere update logs and snapshots are written. Back up both together.
SNAPSHOT_INTERVAL_UPDATESrequired500How many updates before a snapshot is written. Lower means faster loads and more storage.
CURSOR_THROTTLE_MSrequired50Cursor broadcast rate. Low enough to feel live, far below the pointer event rate.
MAX_OBJECTS_PER_BOARDrequired20000A ceiling you enforce, because performance degrades and it is better to say so than to become unusable.

The build, in order

  1. A canvas that stays fast

    Ten thousand objects pan and zoom smoothly because most of them are never drawn.

    1. One transform from world coordinates to screen. Every coordinate conversion goes through it.

      Files src/viewport.ts

      terminal
      npm create vite@latest . -- --template vanilla-ts
    2. Insert object bounds, query by rectangle. It serves both viewport culling and hit testing.

      Files src/quadtree.ts

    3. A fixture board plus a frame time counter. Optimising against two hundred objects teaches you nothing.

      Files src/fixtures.ts

    done when · tick each as it passes
    watch out
    • Rendering every object each frame and relying on the canvas to clip is fast at two hundred objects and unusable at ten thousand, because the cost is in the per-object work rather than the pixels. Cull with the spatial index before drawing, and measure frame time rather than trusting that it feels fine.
  2. Objects and editing

    Sticky notes, shapes, connectors and ink all behave, and text fits its note.

    1. Sticky note, shape, connector, ink and text. Each is data plus a draw function.

      Files src/objects/

    2. Binary search the font size until the wrapped text fits, with a readable floor.

      Files src/text.ts

    3. A line simplification pass with a tolerance. Compare point counts before and after and look at the result.

      Files src/ink.ts

    done when · tick each as it passes
    watch out
    • Freehand ink recorded as every raw pointer sample produces enormous objects and a slow canvas, because a single stroke can be hundreds of points. Simplify each stroke on completion with a tolerance you choose, which typically removes most of the points and no visible detail.
  3. Real multiplayer

    Two people editing at once both keep their work.

    1. Objects as a shared map, sticky text as shared text. Local edits go through it exactly like remote ones.

      Files src/doc.ts

      terminal
      npm install yjs y-websocket
    2. Accept updates, persist them, rebroadcast. It stores and forwards; it never resolves conflicts.

      Files server/relay.ts

    3. Disconnect, edit both sides, reconnect. This is the case that separates real convergence from a demo.

      Files test/converge.test.ts

    done when · tick each as it passes
    watch out
    • Last write wins over a socket looks correct in a demo with one person and destroys work with two, because the loser's edit vanishes with no error. A conflict-free replicated data type is not over-engineering here; it is what makes concurrent editing correct, and retrofitting it later means rewriting every mutation.
  4. Presence and cursors

    You can see where other people are and what they are working on.

    1. Cursor, selection and name. Ephemeral by design, so it must never reach the persisted document.

      Files src/presence.ts

    2. A fixed rate that still reads as live. Measure the resulting message rate.

      Files src/presence.ts

    3. Remove participants on connection loss. A frozen cursor is worse than no cursor.

      Files server/relay.ts

    done when · tick each as it passes
    watch out
    • Sending a cursor update on every pointer move floods the connection with dozens of messages a second per person, and with five participants the transport spends more time on cursors than on edits. Throttle to a rate that still feels live, which is far lower than the pointer event rate.
  5. Persistence and loading a big board

    Opening a board with a long history is fast.

    1. Write a snapshot periodically, then drop the updates it covers. Never drop updates a snapshot does not cover.

      Files server/persist.ts

    2. The client receives one snapshot and the recent updates. Measure load time against a long history.

      Files server/load.ts

    3. Render the board region to an image, and serialise the document to JSON that re-imports.

      Files src/export.ts

    done when · tick each as it passes
    watch out
    • Replaying every update since a board was created works for a week and then takes several seconds, because the update log grows without bound. Snapshot on a schedule and compact behind it, and test load time against a board with a long synthetic history rather than a fresh one.
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.