Skip to content

feat(M4): realtime multiplayer sync + presence over Supabase Realtime#8

Merged
pedrobritx merged 2 commits into
mainfrom
claude/sweet-edison-cVO5d
May 29, 2026
Merged

feat(M4): realtime multiplayer sync + presence over Supabase Realtime#8
pedrobritx merged 2 commits into
mainfrom
claude/sweet-edison-cVO5d

Conversation

@pedrobritx
Copy link
Copy Markdown
Owner

Milestone 4 — Realtime collaboration

Networks the existing Yjs document so edits and presence sync live between everyone on a board. The document was already a CRDT persisted only to IndexedDB; this adds a network transport and awareness on top, with no change to the local-first behavior.

What's included

  • Live document sync — a custom Yjs provider over a Supabase Realtime broadcast channel (no extra server). Implements the Yjs sync-protocol state-vector handshake, so a late-joiner pulls missing updates from connected peers.
  • Presence & live cursors — Yjs awareness carries each user's cursor (world coords), selection, name, and color; remote cursors render in a new non-interactive Konva PresenceLayer.
  • Identity — signed-in users show their email local-part; others are "Guest". Color is deterministic from a stable seed, so the same person is the same color across clients.

How it fits the existing code

  • All network updates are applied with the provider instance as the Yjs transaction origin, so they (a) are not re-broadcast (no echo loop) and (b) are excluded from the UndoManager, which only tracks local null-origin transactions. No change to useUndoManager was needed.
  • The provider attaches after IndexeddbPersistence.whenSynced, so the handshake advertises a complete local state vector and exchanges only a minimal diff.
  • Provider and awareness are per-board singletons (like getIndexedDbProvider), so React StrictMode's double-invoke never opens two channels or duplicates doc.on("update") handlers.

Local-only mode preserved

When VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY are unset, Board passes null to configureRealtime, the provider is skipped, _awareness stays null, and the board works exactly as before against IndexedDB.

Key files

  • packages/sync/src/supabaseProvider.ts (new) — provider, sync + awareness, base64 encoding, origin handling
  • packages/sync/src/identity.ts (new) — deterministic presence colors
  • packages/canvas/src/store/shapeStore.tsconfigureRealtime, _awareness, wires provider after IndexedDB sync
  • packages/canvas/src/layers/PresenceLayer.tsx + hooks/useRemoteCursors.ts + hooks/useAwareness.ts (new)
  • packages/canvas/src/CanvasStage.tsx — publishes local cursor/selection, mounts PresenceLayer
  • apps/web/src/features/canvas/useIdentity.ts (new) + apps/web/src/routes/Board.tsx

Out of scope (noted for follow-up)

  • Server-side snapshot persistence (the snapshots table). Late-joiners rely on connected peers; a joiner with empty local data and no peer online sees an empty board.
  • Large boards can exceed the Supabase broadcast frame size; the provider console.warns past ~200 KB. Chunking the initial sync is a future improvement.

Verification

  • pnpm typecheck — passes across all workspace projects
  • pnpm build — passes
  • Manual multiplayer testing (two windows / incognito): live sync both directions, CRDT convergence, late-joiner handshake, cursors tracking under pan/zoom with constant screen size, identity/color, undo isolation (Ctrl+Z only undoes local ops), and local-only regression with env vars unset.

Config note

Realtime must be reachable by the anon role for notux-board-* broadcast topics. The default (no-authorization) Realtime mode works out of the box; if Realtime Authorization is later enabled, add a policy allowing anon broadcast on those topics.

https://claude.ai/code/session_01U8yLDREdFzmyTk3SxPoTBj


Generated by Claude Code

Adds a custom Yjs network provider that transports document updates and
awareness over a Supabase Realtime broadcast channel scoped per board, plus
live cursors and identity for collaborators.

- packages/sync: SupabaseProvider (Yjs sync protocol state-vector handshake +
  awareness), per-board provider/awareness singletons, deterministic presence
  colors. Remote updates are applied with the provider as transaction origin so
  they neither echo back nor enter the UndoManager.
- packages/canvas: shapeStore.configureRealtime wires the provider in after
  IndexedDB sync; CanvasStage publishes the local cursor/selection to awareness
  and renders remote cursors via a new PresenceLayer.
- apps/web: useIdentity derives a name (signed-in email local-part, else Guest)
  and stable color; Board enables realtime when Supabase is configured.

Degrades to local-only mode unchanged when Supabase env vars are absent.
this.boardId = opts.boardId;
this.doc = opts.doc;
this.awareness = opts.awareness;
this.sessionId = Math.random().toString(36).slice(2);
@pedrobritx pedrobritx marked this pull request as ready for review May 29, 2026 04:22
@pedrobritx pedrobritx merged commit 26bf10a into main May 29, 2026
3 checks passed
@pedrobritx pedrobritx deleted the claude/sweet-edison-cVO5d branch May 29, 2026 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants