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 | null — null 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.