My App
BoardKit

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 components

Data 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 clients

Collaboration Protocol (Socket.IO)

The WebSocket protocol uses typed messages defined in @hfu.digital/boardkit-core:

Connection Flow

  1. Client opens WebSocket to /board namespace
  2. Client sends join with boardId, token, and optional lastSequence
  3. Server authenticates via BoardAuthGuard.validateConnection()
  4. Server responds with joined (participant list + current sequence)
  5. Server sends either sync:full (complete board state) or sync:delta (missed mutations since lastSequence)

Mutation Flow

  1. Client sends mutate with ElementMutation[] and a requestId
  2. Server logs events, increments sequence, persists mutations
  3. Server responds to sender with mutation:ack (requestId + sequence)
  4. Server broadcasts mutation:broadcast to all other clients in the room

Cursor Sharing

  1. Client sends cursor with position and pageId
  2. Server broadcasts cursor:broadcast to all other clients

Reconnection

  • Client tracks lastSequence from ack/broadcast messages
  • On reconnect, sends lastSequence in join
  • Server decides delta sync (< 1000 missed events) or full sync
  • Max 5 reconnect attempts with exponential backoff + jitter

Element Types

ElementToolConstant
strokePenToolTOOL_IDS.PEN
shapeShapeToolTOOL_IDS.SHAPE
textTextToolTOOL_IDS.TEXT
stickyNoteStickyNoteToolTOOL_IDS.STICKY_NOTE
connectorConnectorToolTOOL_IDS.CONNECTOR
image(via useImageImport)--
group(placeholder)--
--SelectToolTOOL_IDS.SELECT
--EraserToolTOOL_IDS.ERASER
--HandTool (pan)TOOL_IDS.HAND
--LaserTool (preview-only)TOOL_IDS.LASER

Session Lifecycle

created → active → idle → archived
StateDescription
createdSession initialized, waiting for first participant
activeAt least one participant connected
idleAll participants disconnected, idle timer running
archivedIdle 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.

On this page