No backend • No database

How it works

Your drawings live in your browser, sync directly between collaborators, and — the moment you set a password — are end-to-end encrypted, so even the relay that ferries them between people can't read a thing. Here's exactly how that holds together, from the top down.

The big picture

What happens between your pen and everyone else's screen.

  1. 1

    You draw

    Your stroke becomes a shape in a document shared by everyone in the room.

  2. 2

    The CRDT records it

    Y.js turns the change into a small, conflict-free update — no central server has to referee.

  3. 3

    Sign & encrypt

    In an encrypted room, the update is signed and AES-256-GCM encrypted right in your browser.

  4. 4

    The relay broadcasts

    A tiny relay forwards the bytes to everyone else. In encrypted rooms it only ever sees ciphertext.

  5. 5

    Peers verify

    Each peer decrypts the frame and checks the signature before it's allowed to change anything.

  6. 6

    Merge & render

    Y.js merges the verified update and the canvas redraws — instantly, in order, on every screen.

  7. 7

    Persist

    Everything is saved locally to IndexedDB, so your work is there next time — even offline.

Under the hood

Each piece in plain language — open “Go deeper” for the real internals.

Real-time sync without a server

Two people can draw at the same instant and never clobber each other's work. That's because the shared document is a CRDT (Conflict-free Replicated Data Type): every change carries enough information to merge cleanly with every other change, in any order, with no server deciding who wins.

Go deeper

The document is a Y.Doc from Y.js: a Y.Map of boards, each holding a Y.Array of shapes. Concurrent edits to the same field resolve last-writer-wins per property, while edits to different properties (one person resizes, another recolors) both survive. Because merging is purely local math, the same model powers offline editing and reconnection. Cursors, names, and live drawing previews ride a separate ephemeral “awareness” channel, so presence never pollutes the saved document.

The relay is deliberately dumb

There is no application server and no database. The only thing in the middle is a ~35-line relay whose entire job is to forward a message from one collaborator to the others. It runs no app logic, stores nothing, and — in encrypted rooms — can't read a single byte it carries.

Go deeper

Sync runs over a PartyKit WebSocket relay (this app does not use WebRTC — a broadcast relay reaches reliably through firewalls and NATs where peer-to-peer mesh stalls). In an encrypted room, every frame is ciphertext before it leaves the browser: document updates, full-state snapshots, cursor positions, names, even the live drawing preview. The one thing advertised in the clear is a small “this room is encrypted” marker, so someone who arrives without a key knows to ask for an invite link instead of joining an unreadable room.

End-to-end encryption

Set a password and the room becomes end-to-end encrypted. The password never travels to any server — it's turned into an encryption key inside your browser, and only people who hold it can read what's drawn.

Go deeper

The password is stretched into a key with PBKDF2 (600,000 iterations of SHA-256, per current OWASP guidance) against a random 16-byte salt minted for that room, then used for AES-256-GCM with a fresh random IV per message. Both the salt and the iteration count travel inside the invite link, so every peer derives the identical key — and the count is clamped to a safe range on decode, so a hand-edited link can neither weaken the derivation below the legacy floor nor pin you to a multi-second one. No key-exchange protocol is needed: the secret is shared out of band, embedded in the link you send.

Capability links & roles

Sharing isn't a single “anyone with the URL can edit” switch. A room can hand out owner, editor, and view-only invite links, and the link itself carries what you're allowed to do.

Go deeper

The capability lives in the URL fragment (everything after the #), which browsers never send to a server — so the relay never sees it. A viewer link carries public keys only; an editor link adds the editor signing key; an owner link adds the owner key on top. The app never emits an owner link — it stays in the creator's address bar — so editors can collaborate but can't lock anyone out. A tampered or truncated link fails closed rather than silently dropping you into an editable room.

View-only that actually holds

A view-only link can't be bypassed by editing the URL or opening dev tools. Even though a viewer can decrypt and see the board, anything they try to draw is rejected by everyone else — because they were never given the key needed to author a change.

Go deeper

In an encrypted room, editors sign each update with a per-epoch ECDSA P-256 key, and every peer verifies that signature before applying the change. Viewers simply hold no signing key, so any update they emit fails verification at every honest peer and never lands in the shared document. Hiding the editing UI for viewers is cosmetic — the real enforcement is the missing key. Each signature also covers the update prefixed by its epoch number, so a signature captured in one epoch can't be replayed after the room rotates.

Owner root-of-trust & revocation

With no server to ask “is this link still allowed?”, revocation is the hard part of a system like this. The answer is a per-room owner key that acts as the root of trust: only the owner can grant editing, and only the owner can take it away.

Go deeper

The owner's key signs an epoch certificate binding the current editor public key to an epoch number; peers only trust an editor key that a valid owner certificate vouches for, and fail closed otherwise. Changing the password rotates the room: the owner mints epoch N+1 with a fresh editor key and certificate and broadcasts an owner-signed rotate notice. Peers mark the old epoch superseded and stop applying its updates, so anyone holding an old link can no longer produce edits that anyone will accept — real revocation, with no central authority.

Offline-first by default

Lose your connection and you can keep drawing. Your work is saved on your own device the whole time, and quietly merges back in when you reconnect.

Go deeper

The Y.Doc is persisted to the browser's IndexedDB via y-indexeddb, which is the source of truth for your local state. Edits made while offline are ordinary CRDT updates; on reconnect they merge conflict-free with whatever changed while you were away. A lightweight per-epoch snapshot cache also lets a freshly joined or reconnecting peer bootstrap quickly without waiting on a live editor to be online.

Hardened against the shared document

In a collaborative app the risky input isn't a form field — it's the shared document itself, which arrives from other people. That untrusted data is sanitized before it ever touches the page, with a strict security policy as the backstop.

Go deeper

Peer-supplied shape fields, names, and presence are run through an allow-list before any of them reach the DOM: colors must be plain hex, numbers must be finite, and unknown tool names collapse to an inert literal. On top of that, a strict Content-Security-Policy (script-src 'self', no inline scripts, base-uri 'none') means even a missed sink can't execute injected markup. It's defense in depth: sanitize the data, then make the browser refuse to run anything that slips past.

What's in the box

The whiteboard you get on top of all that machinery.

See it for yourself

Create a room and invite your team. It takes 2 seconds.

Start Drawing