Private Project
Private Collab Whiteboard
Last updated Feb 2026
Real-time collaborative whiteboard with P2P sync, end-to-end encryption, and infinite canvas
Architecture Overview
System Diagram
flowchart TD
subgraph Browser["Browser (Client)"]
LP[Landing Page<br>index.html]
BP[Boards Page<br>boards.html]
RP[Room Page<br>room.html]
subgraph AppCore["Application Core"]
APP[app.js<br>Entry Point]
DRW[drawing.js<br>Canvas Rendering]
BRD[boards.js<br>Board Manager]
AWR[awareness.js<br>User Presence]
UND[undo-redo.js<br>History Manager]
MOD[modal.js<br>Dialog System]
RM[room-manager.js<br>Room & Auth]
end
subgraph SyncLayer["Sync Layer"]
YJS[yjs-setup.js<br>Y.Doc Init]
SP[sync-provider.js<br>WebSocket Client]
CRY[crypto.js<br>E2E Encryption]
end
subgraph Storage["Local Storage"]
IDB[(IndexedDB<br>y-indexeddb)]
LS[(localStorage<br>Board History)]
end
end
subgraph Server["PartyKit Server"]
PK[party/index.ts<br>Message Relay]
end
LP --> RP
BP --> RP
RP --> APP
APP --> DRW
APP --> BRD
APP --> AWR
APP --> UND
APP --> MOD
APP --> RM
APP --> YJS
YJS --> SP
YJS --> CRY
YJS --> IDB
SP <-->|WebSocket<br>Encrypted/Plain| PK
PK <-->|Broadcast to<br>Other Clients| SP
RM --> LS
APP --> LS
DRW --> AWR
BRD --> AWR
Data Flow Diagram
sequenceDiagram
participant User
participant Canvas as drawing.js
participant YDoc as Y.js Document
participant IDB as IndexedDB
participant Provider as sync-provider.js
participant Crypto as crypto.js
participant Server as PartyKit
participant Peer as Remote Peer
User->>Canvas: Draw shape
Canvas->>YDoc: Append to Y.Array
YDoc->>IDB: Persist locally
YDoc->>Provider: Encode update
alt Encrypted Room
Provider->>Crypto: Encrypt update (AES-256-GCM)
Crypto->>Provider: Encrypted bytes
end
Provider->>Server: Send via WebSocket
Server->>Peer: Broadcast to others
alt Encrypted Room
Peer->>Crypto: Decrypt update
Crypto->>Peer: Plaintext update
end
Peer->>YDoc: Apply update (CRDT merge)
YDoc->>Canvas: Observer triggers redraw
Component Descriptions
app.js — Main Entry Point
- Purpose: Orchestrates initialization and wires all modules together
- Location:
js/app.js - Key responsibilities: Tool switching with per-tool settings persistence, keyboard shortcuts, zoom/pan controls, side panel management, canvas resizing, browser compatibility checks, global error handling
drawing.js — Canvas Rendering Engine
- Purpose: Handles all HTML5 Canvas drawing, hit-testing, shape manipulation, and live drawing previews
- Location:
js/drawing.js - Key responsibilities: Rendering shapes from Y.Array data, mouse/touch interaction handling, shape selection (single and multi-select), move/resize operations, copy/paste/duplicate, read-only mode enforcement, remote cursor rendering
yjs-setup.js — Y.js Initialization
- Purpose: Creates the Y.Doc, sets up IndexedDB persistence and the WebSocket sync provider
- Location:
js/yjs-setup.js - Key responsibilities: Y.Doc creation, IndexedDB provider setup, encryption key derivation (if password provided), network sync with timeout, default board creation (only after sync to prevent duplicates)
sync-provider.js — WebSocket Sync Provider
- Purpose: Custom Y.js sync provider using PartyKit WebSockets with optional E2E encryption
- Location:
js/sync-provider.js - Key responsibilities: WebSocket connection management with auto-reconnect, Y.js sync protocol encoding/decoding, awareness protocol for user presence, optional encrypt/decrypt wrapping of all messages, periodic resync for reliability
awareness.js — User Presence
- Purpose: Manages ephemeral user state (cursors, names, colors, active board, in-progress drawings)
- Location:
js/awareness.js - Key responsibilities: Local user state broadcasting, remote cursor rendering with labels, user list UI, live drawing preview propagation, throttled cursor updates to reduce network traffic
boards.js — Multi-Board Manager
- Purpose: Manages multiple boards (whiteboards) within a single room
- Location:
js/boards.js - Key responsibilities: Board creation and switching, board tab UI rendering, board clearing, awareness sync of current board per user
crypto.js — Encryption Utilities
- Purpose: End-to-end encryption using Web Crypto API
- Location:
js/crypto.js - Key responsibilities: PBKDF2 key derivation (100K iterations, SHA-256), AES-256-GCM authenticated encryption, random IV per message, password verification
room-manager.js — Room & Access Control
- Purpose: Handles room URLs, password tokens, and permission levels
- Location:
js/room-manager.js - Key responsibilities: Room ID extraction from URL, access token encoding/decoding with signature verification, shareable link generation, read-only mode detection, clipboard operations
undo-redo.js — History Manager
- Purpose: CRDT-aware undo/redo using Y.js UndoManager
- Location:
js/undo-redo.js - Key responsibilities: Only tracks local user's changes (never undoes other users' work), 500ms grouping window for rapid changes, stack state notifications for UI updates
modal.js — Dialog System
- Purpose: Custom modal system replacing native browser dialogs
- Location:
js/modal.js - Key responsibilities: Prompt, confirm, alert dialogs, invite modal, password modal, keyboard shortcuts help, text extraction modal
board-history.js — Board History Manager
- Purpose: Persists room visit history in localStorage
- Location:
js/board-history.js - Key responsibilities: Save/load board history, role tracking (owner/collaborator/viewer) with owner preservation, access counting, relative time formatting
config.js — Configuration Constants
- Purpose: Centralizes all magic numbers and configurable values
- Location:
js/config.js - Key responsibilities: Zoom limits, timing intervals, crypto parameters, drawing defaults, user color palette
party/index.ts — PartyKit Server
- Purpose: Minimal WebSocket message relay
- Location:
party/index.ts - Key responsibilities: Accept WebSocket connections, broadcast messages to all other clients in the room (never echoes back to sender). Only 35 lines — all logic lives client-side.
External Integrations
| Service | Purpose | Documentation |
|---|---|---|
| PartyKit | WebSocket message relay for real-time sync | partykit.io |
| Vercel | Static site hosting with URL rewrites | vercel.com |
| Google Fonts | Inter typeface for UI | fonts.google.com |
Key Architectural Decisions
CRDTs over Operational Transform
- Context: Needed conflict-free real-time collaboration without a central authority
- Decision: Y.js CRDT library
- Rationale: CRDTs guarantee eventual consistency without a server. Unlike OT (used by Google Docs), CRDTs work offline and don't need a central server to resolve conflicts. Y.js is the most mature CRDT implementation for JavaScript.
PartyKit over Raw WebRTC
- Context: Originally used y-webrtc for peer-to-peer connections, but WebRTC has NAT traversal issues
- Decision: Migrated to PartyKit WebSocket relay
- Rationale: PartyKit provides reliable message delivery without NAT/firewall issues. The server is a simple broadcast relay (doesn't see decrypted data), maintaining the privacy model while improving reliability.
Client-Side Encryption
- Context: Wanted true E2E encryption where the relay server never sees plaintext
- Decision: AES-256-GCM encryption with PBKDF2 key derivation, all in the browser
- Rationale: The Web Crypto API provides hardware-accelerated crypto. By encrypting Y.js sync messages before sending, the PartyKit server only ever sees ciphertext. Password-derived keys mean no key exchange protocol is needed — users share the password out of band.
Multi-Page Application (MPA) over SPA
- Context: The app has three distinct pages (landing, boards list, whiteboard room)
- Decision: Vite MPA mode with separate HTML entry points
- Rationale: Each page has very different functionality. MPA keeps bundles small (the landing page doesn't load Y.js), and Vite's MPA support handles the routing cleanly. URL rewrites in Vercel handle the
/room/:idpattern.
IndexedDB for Local Persistence
- Context: Users need to return to their whiteboards even without network
- Decision: y-indexeddb for Y.Doc persistence, localStorage for board history
- Rationale: IndexedDB handles the potentially large binary Y.Doc state, while localStorage is simpler for the small structured board history data. y-indexeddb integrates directly with Y.js for seamless offline support.