A password-protected, real-time retrospective board. Columns for Happy / Idea / Sad / Action; vote, discuss with a 5-minute timer, end the retro to archive it. Sync happens peer-to-peer between browsers on the same URL with the same password — no server sees your plaintext.
- CRDT state. A Yjs document per board holds the items, votes, archives, and who's currently being discussed. Concurrent edits merge intent-preservingly.
- Transport.
y-webrtccarries Yjs updates directly between peers via WebRTC data channels. Peers find each other through a tiny WebSocket signaling server; everything on the wire is encrypted with an AES-GCM key derived from the board password. - Persistence.
y-indexeddbmirrors the doc to each browser's local storage, so a board is still usable offline and restores on reopen. - Auth model. The password never leaves the browser. A
slug-derived canary value, encrypted with the password-derived key,
tells a joining peer whether the password they typed matches the one
everyone else is using — without needing to decrypt live traffic to
find out.
src/board/session.tshas the details.
The feature contract is in features.md; every live
statement there is asserted by an end-to-end Playwright test.
npm install
npm run dev # Vite at http://localhost:5173
npm run signaling # y-webrtc signaling server on :4444 (separate terminal)Open two browsers at http://localhost:5173/b/<any-slug> and use the same
password; they'll sync in real time.
npm run test:e2e # Playwright against a one-shot signaling server + dev server
npm run check:features # every feature ID in features.md has a matching testDeploys to Cloudflare as a single Worker with a static-assets binding and a
Durable Object for the signaling server. Production hostname is
retro.re-cinq.com (declared in wrangler.toml).
npx wrangler login # one-time, OAuth in a browser
npm run deploy # builds dist/ and pushes the WorkerFirst deploy creates the Worker, applies the v1 Durable Object migration,
uploads dist/ to the assets binding, and attaches the custom domain
automatically (the parent zone is already on the same Cloudflare account).
Stream live Worker logs with npx wrangler tail.
The TURN relay for users behind symmetric NAT / strict firewalls is not yet
wired; see plans/cloudflare-bringup.md
for the second-cut plan.
When testing P2P sync across two Chrome profiles on the same host, each
profile's WebRTC offer carries anonymised *.local mDNS hostnames instead of
your real local IP. The profiles can't resolve each other's mDNS names, ICE
never picks a candidate pair, and the app shows "Wrong password" after the 5 s
canary timeout — even when the password is correct.
Workaround: in each profile visit
chrome://flags/#enable-webrtc-hide-local-ips-with-mdns, set it to
Disabled, restart Chrome. Two tabs within a single profile don't need this
— they sync via the shared IndexedDB broadcast channel. Doesn't affect real
deployments.
Licensed under the AI Native Application License (AINAL) v2.0.