FrameLab
← Portfolio Case Study

02 Real-time systems · Solo build

Everyone edits at once.
Nobody edits above their role.

FrameLab is a live, real-time collaborative canvas — Figma-shaped: an infinite surface where a team drops shapes, leaves comments, and watches each other's cursors move. I designed and built it solo, end to end. The interesting part isn't that it syncs; it's that a Viewer still can't write a shape, even with a raw socket and a hand-crafted packet.

FrameLab's canvas editor — shapes on the crease-lattice grid with a comment pin and the floating tool cluster
CANVAS EDITOR LIVE PRODUCT

Status

LIVE

deployed & running today

Built by

Solo

design → ship, end to end

Enforcement

ON THE WIRE

not hidden in the UI

Real-time collaboration is the easiest thing to fake.

Most collaborative demos broadcast state to everyone and call it multiplayer — read-only means a disabled button, and the server trusts whatever the client sends. That holds up exactly until someone opens devtools. Two things are genuinely hard: merging concurrent edits from several people without a central lock and without losing work, and making a permission mean something at the layer where edits actually travel. FrameLab treats both as the product.

A collaborative canvas with its guarantees intact.

A Next.js app on Vercel that owns sign-in and is the only thing with authority over Postgres; a separate always-on Node process on Railway holding one websocket per connected browser, relaying and policing edits; and a shared Postgres for who-owns-what plus a periodic snapshot of each canvas. The canvas itself is raw Canvas 2D — no canvas library — with the maths in a zero-dependency package that's property-tested. Concurrent edits merge through a CRDT (Yjs) rather than a lock. 197 unit tests and about 30 Playwright end-to-end tests, including a multiplayer spec that drives two browsers at once. Built in numbered phases over six weeks, each with a written design spec before any code — and it's deployed and running right now, not sitting behind a git clone waiting to be demoed.

Live cursors, two sessions, a role revoked mid-edit


Two services, one database, one hard rule.

Postgres owns anything that needs authority — who you are, who owns a document, who's allowed to edit it. A conflict-free replicated document (Yjs) owns anything with concurrent multi-writer edits. The relay in the middle is the only place a role can be enforced, because it's the one participant the editing client doesn't control — a Viewer could otherwise grant themselves Editor by writing the change into their own CRDT replica, so the check has to live outside the CRDT, on the server relaying it.

FrameLab architecture The browser signs in to the Next.js app on Vercel, which reads and writes Postgres and mints a short-lived room token. The browser opens a websocket to the Node relay on Railway, which verifies the token, enforces the connection's role on every edit, relays edits to other browsers, and flushes periodic snapshots to Postgres. The web app can also invalidate a live connection on Postgres, forcing it to reconnect with a fresh role. Sign in · token Authority Edits · role-checked Snapshot · 2s/10s POST /internal/invalidate Browser Y.Doc · IndexedDB apps/web — Vercel Next.js · Auth.js · sole DB authority apps/server — Railway WS relay · enforces role per edit Postgres users · roles · snapshots

The split — authority left, concurrency right


Four decisions worth knowing about.

01

A write gate that fails closed

If your role says you can't edit, you can't edit — not "the buttons are greyed out," but the server refuses the bytes and repairs the document.

The gate starts unchecked, not not-violated, so a listener that never fires rejects the write instead of waving it through. Rejected updates are reverted with a zero-timeout UndoManager, and the correction is sent back to the offending connection alone.

02

Two exploits I had to find first

The obvious version of this check passes review and still leaks. I broke my own gate twice before it held.

Yjs integrates structs before reading an update's trailing delete set, so truncating one byte let a Viewer's shapes land and then throw. Separately, updates referencing unintegrated clocks get buffered and replayed later under a different connection's role — the gate now treats any buffering as illegitimate and fails closed.

03

Offline is a guarantee, not a spinner

Close the laptop mid-edit on a dead network, open it tomorrow, and the canvas is exactly where you left it.

The Y.Doc persists to IndexedDB, and the boot order is deliberate: attach local storage, await whenSynced, paint, then dial the websocket. Snapshots flush to Postgres 2s after the last edit, 10s hard cap — and a room flushes before it's evicted, or a reconnect mid-flush would silently roll the document back.

04

A canvas that doesn't re-render

Dragging a shape stays smooth with a room full of people in it.

Shapes live outside React entirely — pushed straight to the renderer, so a drag never re-renders a component. In-flight gestures sit in a ref, two overlay layers merge per frame so a mid-drag React update can't wipe the preview, and device pixel ratio is clamped at 2.

Let's build your next one.

Open the live demo →

Source on GitHub