Architecture
Design principles, data flow, and collaboration protocol of BoardKit
Design Principles
Pluggable Storage
All database operations go through abstract storage classes (BoardStorage, AssetStorage, EventLogStorage). Implement these interfaces for any database. A Prisma adapter is included, but the backend package never imports @prisma/client directly — adapters use structural typing only.
Zero-Dependency Core
@hfu.digital/boardkit-core has zero runtime dependencies. It contains all shared types, the scene graph, tools, collaboration protocol, and validation logic. Both the backend and frontend depend on it.
Headless-First Frontend
Every React hook exposes the full state and actions. Pre-built components are optional wrappers around these hooks. Use BoardCanvas and Toolbar for rapid prototyping, or build your own UI with useTool, useViewport, and the other hooks.
Mutable Store Outside React
BoardStore lives outside React's state management (no useState or useReducer). It uses a slice-based pub/sub pattern where hooks bridge into React via useSyncExternalStore. This avoids unnecessary re-renders and keeps the canvas rendering pipeline fast.
Dual-Canvas Rendering
Two stacked <canvas> elements handle rendering:
- Static layer — Re-renders only when the scene changes (nonce invalidation). Two-pass: non-connectors first, then connectors.
- Interactive layer — Re-renders every animation frame for cursors, selections, previews, and alignment guides.
Package Architecture
@hfu.digital/boardkit-core (zero deps)
├── Types: Board, Page, Element, Participant, Protocol
├── Scene graph: immutable SceneState mutations
├── Tools: abstract Tool class with state machine
├── Operations: LWW merge, History, Clipboard
└── Validation, Grid, Alignment
@hfu.digital/boardkit-nestjs (depends on core)
├── BoardModule.register(options)
├── 5 REST controllers
├── Socket.IO WebSocket gateway
├── Domain services (Board, Collaboration, Permission, Asset, Export)
├── Abstract storage interfaces
└── Testing utilities (InMemory adapters)
@hfu.digital/boardkit-react (depends on core)
├── BoardKitProvider (context with Store + Renderer + ToolRegistry)
├── BoardStore (mutable, slice-based pub/sub)
├── Canvas2DRenderer (dual static/interactive layers)
├── InputPipeline + GestureRecognizer
├── 13 hooks (useBoard, useCollaboration, useTool, etc.)
└── 11 pre-built componentsData Flow
PointerEvent → InputPipeline → GestureRecognizer → Tool.onPointerX()
→ ToolResult { mutations, preview }
→ BoardStore.updateScene() [applies mutations]
→ Canvas2DRenderer.renderStaticLayer() [nonce invalidation]
→ useCollaboration sends mutations via Socket.IO
→ Server applies LWW merge, broadcasts to other clientsCollaboration Protocol (Socket.IO)
The WebSocket protocol uses typed messages defined in @hfu.digital/boardkit-core:
Connection Flow
- Client opens WebSocket to
/boardnamespace - Client sends
joinwithboardId,token, and optionallastSequence - Server authenticates via
BoardAuthGuard.validateConnection() - Server responds with
joined(participant list + current sequence) - Server sends either
sync:full(complete board state) orsync:delta(missed mutations sincelastSequence)
Mutation Flow
- Client sends
mutatewithElementMutation[]and arequestId - Server logs events, increments sequence, persists mutations
- Server responds to sender with
mutation:ack(requestId + sequence) - Server broadcasts
mutation:broadcastto all other clients in the room
Cursor Sharing
- Client sends
cursorwithpositionandpageId - Server broadcasts
cursor:broadcastto all other clients
Reconnection
- Client tracks
lastSequencefrom ack/broadcast messages - On reconnect, sends
lastSequenceinjoin - Server decides delta sync (< 1000 missed events) or full sync
- Max 5 reconnect attempts with exponential backoff + jitter
Element Types
| Element | Tool | Constant |
|---|---|---|
stroke | PenTool | TOOL_IDS.PEN |
shape | ShapeTool | TOOL_IDS.SHAPE |
text | TextTool | TOOL_IDS.TEXT |
stickyNote | StickyNoteTool | TOOL_IDS.STICKY_NOTE |
connector | ConnectorTool | TOOL_IDS.CONNECTOR |
image | (via useImageImport) | -- |
group | (placeholder) | -- |
| -- | SelectTool | TOOL_IDS.SELECT |
| -- | EraserTool | TOOL_IDS.ERASER |
| -- | HandTool (pan) | TOOL_IDS.HAND |
| -- | LaserTool (preview-only) | TOOL_IDS.LASER |
Session Lifecycle
created → active → idle → archived| State | Description |
|---|---|
created | Session initialized, waiting for first participant |
active | At least one participant connected |
idle | All participants disconnected, idle timer running |
archived | Idle timeout reached; persistent sessions get a snapshot |
Timeouts:
- Ephemeral sessions: 5-minute idle timeout, no archival snapshot
- Persistent sessions: 15-minute idle timeout, snapshot created on archive
Permission Model
viewer < editor < owner- viewer — Can view the board and join WebSocket sessions (read-only)
- editor — Can create, update, and delete elements
- owner — Full control including board settings, member management, and sharing
Board owners are automatically granted full access. Permissions are enforced in all 5 REST controllers and the WebSocket gateway.