Skip to content

How the Engine Fits Together

This page is an orientation for building on DocuShark — just enough of the architecture to write a shape, a tool, a UI feature, or a client. It is not an exhaustive internal reference; for the last word, read the source.

Extension surface vs. engine core

DocuShark is AGPL, so every line is readable — but "readable" and "a supported extension surface" are different things. The parts we document here and want you building on are the edges: custom shapes, tools, UI features / panels, and clients on the public REST + MCP API. The engine core and the relay's CRDT/sync internals are deliberately not documented as an extension surface — they change without notice and aren't a supported contribution path. Extend at the edges; build on the public API.

Technology Stack

LayerTechnology
RuntimeBun (package manager, JS runtime)
DesktopTauri v2 (Rust backend)
LanguageTypeScript (strict), Rust
UI FrameworkReact 18+
CanvasCanvas 2D API (pure, no libraries)
StateZustand + Immer
CollaborationYjs CRDTs over WebSocket
Rich TextTiptap (ProseMirror)
Spatial IndexRBush (R-tree)
BuildVite (frontend), Cargo (Rust)

The Layers

  • React UI renders only the chrome (toolbar, panels, modals) — never the canvas. App.tsx, Toolbar.tsx, PropertyPanel.tsx, LayerPanel.tsx, and the bridge CanvasContainer.tsx.
  • Engine core is pure TypeScript with no React dependency: Camera, Renderer, InputHandler, ToolManager, SpatialIndex, HitTester, plus the ShapeRegistry (which lives under src/shapes/). The render loop is a requestAnimationFrame cycle — this is why large diagrams stay smooth.
  • Stores are Zustand + Immer, split by responsibility (below).
  • Storage is hybrid: localStorage for metadata/preferences, IndexedDB for binary blobs via BlobStorage.ts (content-addressed SHA-256, dedup, GC).
  • Tauri backend (src-tauri/) is the desktop shell only — native file system + windowing. It is a pure client and does not run the collaboration server.
  • Relay (relay/, Axum + Tokio) owns collaboration: the WebSocket sync channel, an authoritative server-side Y.Doc per active document, document + blob storage, REST, the MCP endpoint, and OIDC token validation (it verifies external JWTs against a JWKS — it never mints tokens). See Wire Protocol & Building a Client.

Shapes Are Data

Shapes are plain, JSON-serializable objects with no methods. All behaviour is external, registered per shape type with the ShapeRegistry. A handler provides five functions — render, hitTest, getBounds, getHandles, create (plus optional getLabelEditTarget / renderOverlay):

  • render(ctx, shape) — the canvas context is already transformed to world space, so you draw in world coordinates and never touch the camera here.
  • create(position, id) — build a new shape instance at a point.

Every shape extends BaseShape: id, type (a ShapeType), x, y, fill / stroke (string | nullnull means "none"), strokeWidth, opacity, rotation, locked, visible. Size and shape-specific fields live on the concrete type (e.g. width/height on a rectangle, radiusX/radiusY on an ellipse) — not on BaseShape. Full field reference: Shape Properties. To author one: Creating Custom Shapes.

The Coordinate System

The Camera class owns all coordinate math — never apply pan/zoom by hand.

  • camera.screenToWorld(point) / camera.worldToScreen(point) convert between the canvas pixels an event carries and the infinite world plane your shapes live on.
  • camera.getVisibleBounds() returns the visible world rectangle — used for viewport culling.
  • The renderer composes camera.getTransformMatrix() onto the device-pixel (DPI) transform once per frame, then draws every shape in world space.

Tools Are State Machines

A tool reacts to normalised pointer/keyboard events and holds its own interaction state. It receives a ToolContext of granular accessor functions — not the raw stores — to stay decoupled: getShapes / getShapeOrder, addShape / updateShape / deleteShapes, select, setCursor, setActiveTool, pushHistory, plus camera, hitTester, and requestRender. Register a tool with toolManager.register(new MyTool()). To author one: Creating Custom Tools.

The Stores

Zustand + Immer; mutate only through Immer, never in place.

StoreResponsibility
documentStoreShapes (connectors and groups are shapes) + their z-order (shapeOrder) — the single source of truth for content
sessionStoreEphemeral UI state: selectedIds (a Set), camera, hoveredId, activeTool, cursor
historyStoreUndo/redo — push(description?) snapshots the document; undo() / redo()
pageStoreMulti-page structure and ordering
persistenceStoreSave/load, auto-save, localStorage

Collaboration adds relayDocumentStore (workspace/relay-stored documents; cached offline by RelayDocumentCache) and userStore (identity + JWT). Feature stores (theme, style profiles, palettes, settings, …) each live in their own file under src/store/.

Extension Points

ExtensionMechanismGuide
Custom shapesRegister handlers with ShapeRegistryCreating Custom Shapes
Custom toolsRegister with ToolManagerCreating Custom Tools
UI features / panelsThe PanelExtensions.ts registryBuilding UI Features
Shape librariesCollections under src/shapes/library/Creating Custom Shapes
Export formatsAdd exporters in exportUtils.tsUtility Modules
PDF node typesRegister with PDFNodeRendererRegistry in pdfExportUtils.tsCreating Prose Helpers
Prose (Tiptap) helpersShared extensions in TiptapEditor.tsxCreating Prose Helpers

Next Steps

Released under the AGPL-3.0 License.