Kumitate
Kumitate (組み立て, Japanese for "assembly") is a collaborative 3D brick builder that runs entirely in the browser. You open a room, send someone the link, and you are both building in the same scene in real time, with each other's cursors, ghost bricks and names visible live. There is no sign-up, no install and no database behind it.
Open It and Build
The real deployed builder, embedded live. It drops you straight into a room, so you can place a brick right here. "Copy invite link" gives you a URL anyone can open to join the same scene.
The Interesting Problem
Two people clicking the same empty cell at the same instant. A lock solves it and makes every single placement feel laggy.
So placement is optimistic: your brick appears the moment you click it, and the conflict gets resolved after the merge rather than prevented before it. The resolution rule is a pure function of the merged document: sort the bricks by id, replay them into a fresh occupancy index, and drop whatever no longer fits. Every client runs that independently, with no coordination at all, and they all arrive at the same answer.
It runs in two places on purpose. On the client, so the correction is immediate and the person who lost gets told why their brick vanished rather than watching it silently disappear. On the server, authoritatively, so a client that does not implement the rule cannot leave a room permanently broken for everyone else in it.
Architecture
Three parts, split along a line that matters: the shared logic has no 3D or React dependencies, because the server imports it too.
brick-core
Pure logic, no rendering: the part catalogue, colour palette, integer grid maths, occupancy index, collision test, conflict resolution and zod schema. Dependency-free on purpose, since the server runs the same resolution rule the browser does.
Static Front End
A Next.js static export on GitHub Pages, rendering through React Three Fiber. One InstancedMesh per part carrying per-instance colour, so the scene draws in roughly 16 calls no matter how many bricks are in it.
Realtime Server
One Cloudflare Durable Object per room, holding that room's Yjs document, fanning updates out over WebSockets and snapshotting to SQLite storage so a room survives everyone closing the tab.
Design Decisions
Positions are integers, end to end
x and z in studs, y in plates, with one brick equal to three plates. Floats only appear at the render boundary, so two concurrent edits cannot drift apart by a rounding error and then disagree about whether a cell is occupied.
Undo is scoped per user
A Y.UndoManager bound to the local origin, so pressing undo can only ever remove your own bricks. Shared undo in a room where two people are building at once is a way to delete someone else's work by accident.
One InstancedMesh per part, colour per instance
The obvious split is one mesh per part per colour, which at 16 parts and 12 colours is 192 draw calls. Moving colour onto the instance attribute collapses that to roughly 16, independent of how many bricks the room holds.
The catalogue keys on canonical part IDs
All 16 parts and 12 colours were checked against the official LDraw library, and two IDs in my own spec turned out to be stale: Plate 1x2 moved to 3023b in 2024, and Plate 1x1 Round to 6141 in 2025. The catalogue keys on the canonical IDs and keeps the legacy ones as aliases, so older saved builds still load.
Nothing to keep warm
GitHub Pages plus the Cloudflare Workers free tier, with no database anywhere. A room's state lives in its Durable Object and is snapshotted to that object's own storage, which is what makes the whole thing cost nothing to run.
Testing
156 automated tests, and the ones that matter are not the unit tests.
Anything built on a CRDT fails in ways a single-document unit test cannot see, so the valuable tests wire two real Yjs documents together the way the network wires them. One proves undo stays scoped to its own origin when both documents are live. Another deliberately partitions the pair, lets both sides place a brick in the same cell, then reconnects them and asserts that the two documents converge on the same result without either being told what the other did.
A separate script goes further out and drives real WebSocket clients through the deployed Cloudflare worker, so the server's authoritative copy of the resolution rule is exercised against the actual deployment rather than a local stub.
Also In There
Presence: names, per-user colours, live cursors and the ghost brick showing where someone is about to place. Starter templates so an empty room is not a blank grid. A reveal animation that assembles a finished build brick by brick. And versioned JSON export and import, so a build is a file you own rather than something trapped in a room.
What This Proves
Distributed Systems Reasoning
Chose eventual consistency with a deterministic, document-pure resolution rule over locking, and can explain the trade-off: optimistic placement keeps every click instant, and convergence is guaranteed because every client computes the same answer from the same merged state.
Defensive Server Design
The resolution rule runs client-side for feedback and server-side for authority, so a misbehaving or outdated client cannot corrupt a room for everyone else in it.
Real-Time Graphics Performance
Instanced rendering with per-instance colour holds the scene at roughly 16 draw calls regardless of brick count, instead of the 192 a naive part-by-colour split would cost.
Testing What Is Actually Hard
Wired two live Yjs documents together and deliberately partitioned them to prove undo scoping and conflict convergence, rather than unit-testing the easy pure functions and calling it covered.
Open a room, build something, and send the link to someone else:
Open Kumitate