Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

tldraw SDK — Admin & Architecture Guide

Generated June 2026 from tldraw/tldraw at 5e29f4e

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is for the engineers, architects, and operators who build with tldraw and who stand up the infrastructure around it: the reactive runtime, the package graph, the extension model, the multiplayer protocol, and the production reference architecture (tldraw.com) that the SDK's own team runs. It is written to be exhaustive enough that a competent engineer — or an AI — could reconstruct the system's design, deploy a self-hosted collaborative backend, and reason about its security and performance characteristics without reading the source.

Where the User Guide answers "how do I use the component," this guide answers "how is it built, how does it scale, and how do I operate it."


Table of Contents

  1. Executive Summary
  2. System Architecture
  3. The Monorepo & Package Graph
  4. The Reactive Core: Signals → Store → Schema
  5. The Editor Runtime
  6. Extension Model Internals
  7. Persistence & Document Model
  8. Multiplayer Architecture (@tldraw/sync)
  9. Self-Hosting a Sync Backend
  10. The tldraw.com Reference Architecture
  11. Other Products in the Repo
  12. Configuration & Environment Reference
  13. Build, Release & Versioning
  14. Security Analysis
  15. Performance & Scaling
  16. Operations Runbook
  17. Code Quality & Conventions
  18. Potential Improvements
  19. Quick Reference

1. Executive Summary

What tldraw is. tldraw is a React SDK for building infinite-canvas applications. The flagship deliverable is the tldraw npm package (v5.1.0): a single <Tldraw> component that mounts a complete, extensible whiteboard editor. Around that flagship the repository ships a CLI scaffolder (create-tldraw), a multiplayer library (@tldraw/sync), the production whiteboard app (tldraw.com), a VS Code extension, an MCP server, and eight starter-kit templates.

Architectural thesis. tldraw renders the canvas to the DOM, not to a <canvas>/WebGL surface. This is the defining design choice: shapes are React components, so they can contain arbitrary HTML, SVG, iframes, and app-specific UI, and the editor inherits the browser's accessibility, text, and embedding capabilities. The cost — managing thousands of DOM nodes performantly — is paid back through a custom fine-grained reactivity system (signals), spatial indexing, culling, and level-of-detail (LOD) rendering.

State model. Everything is data in a single reactive store of typed records (shapes, bindings, assets, pages, camera, instance state, presence). The store sits on a signals library (@tldraw/state) that gives derived values, transactions, and history for free. Persistence, undo/redo, multiplayer, and serialization are all expressed as operations over this one store.

Operational footprint for the SDK itself. Zero. The published package is client-side React with no server requirement. Server infrastructure enters only when you opt into multiplayer or server-side document persistence — and tldraw provides both a reference protocol (@tldraw/sync-core) and a reference deployment (Cloudflare Durable Objects, used by tldraw.com and the Multiplayer starter kit).

Attribute Value
Flagship package tldraw v5.1.0
Language TypeScript (ESM)
UI runtime React 18.2+ / 19.2.1+, DOM rendering
Reactivity Custom signals (@tldraw/state)
License tldraw license (production key required)
Repo scale ~3,950 files, ~6 MB source, monorepo of ~20 packages + apps + templates
Multiplayer reference Cloudflare Workers + Durable Objects + Postgres
Build tooling yarn (corepack), lazyrepo, custom build scripts, API Extractor

2. System Architecture

Layered view of the SDK

┌──────────────────────────────────────────────────────────────┐
│  Your React App                                               │
│    <Tldraw> / <TldrawEditor> / <TldrawImage>                  │
└───────────────┬──────────────────────────────────────────────┘
                │ props: shapeUtils, tools, components, options…
┌───────────────▼──────────────────────────────────────────────┐
│  tldraw  (full SDK)                                            │
│    default shapes/tools/bindings · full UI (menus, toolbar,   │
│    panels, dialogs) · clipboard · external content · .tldr    │
└───────────────┬──────────────────────────────────────────────┘
┌───────────────▼──────────────────────────────────────────────┐
│  @tldraw/editor  (engine)                                     │
│    Editor class · managers (history, snap, text, camera…) ·   │
│    ShapeUtil/BindingUtil/StateNode base classes · geometry ·  │
│    Canvas renderer · hooks · LicenseManager                   │
└──────┬───────────────┬───────────────┬───────────────┬───────┘
       │               │               │               │
┌──────▼─────┐ ┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ @tldraw/   │ │ @tldraw/store│ │ @tldraw/    │ │ @tldraw/    │
│ tlschema   │ │ reactive     │ │ state +     │ │ validate    │
│ record/    │ │ record store │ │ state-react │ │ runtime     │
│ shape defs │ │ + queries    │ │ signals     │ │ validators  │
└────────────┘ └──────────────┘ └─────────────┘ └─────────────┘

Data-flow view

User input (pointer/keyboard/touch)
   │
   ▼
StateNode chart (current tool)  ──emits──►  Editor commands
   │                                            │
   ▼                                            ▼
Editor API  ──writes──►  Store (records)  ──reactivity──►  Canvas re-render
   │                          │                                (only changed shapes)
   │                          ├──► IndexedDB (persistenceKey)
   │                          ├──► TLSyncClient ──WebSocket──► sync server
   │                          └──► snapshot / .tldr serialization
   ▼
Side effects (before/after create/update/delete) + history (undo/redo)

Every interaction becomes a store mutation; every store mutation propagates reactively to the canvas, to persistence, and (if connected) to the sync client. There is one source of truth.


3. The Monorepo & Package Graph

tldraw is a yarn-workspaces monorepo. The repo is organized into packages/ (published libraries), apps/ (deployed applications), templates/ (starter kits), internal/ (build/dev tooling), and assets/ (fonts, icons, translations, watermarks).

Published packages (packages/)

Package Role Notable exports
tldraw The full SDK (flagship) Tldraw, default shapes/tools/UI, .tldr I/O
@tldraw/editor Core engine Editor, ShapeUtil, BindingUtil, StateNode, managers, geometry
@tldraw/store Reactive record store Store, RecordType, StoreSchema, StoreQueries, StoreSideEffects
@tldraw/state Signals reactivity atom, computed, transact, EffectScheduler, Transaction
@tldraw/state-react React bindings for signals useValue, track, useAtom
@tldraw/tlschema Record & shape schema, migrations TLShape, TLRecord, record types, style props
@tldraw/validate Runtime validation Validator, T.object, T.string, …
@tldraw/sync Multiplayer React hooks useSync, useSyncDemo
@tldraw/sync-core Multiplayer protocol TLSyncClient, TLSyncRoom, TLSocketRoom, socket adapters, storage
@tldraw/utils Shared utilities LruCache, ExecutionQueue, PerformanceTracker, FileHelpers
@tldraw/driver Low-level driver glue Driver
@tldraw/mermaid Mermaid diagram support createMermaidDiagram
@tldraw/dotcom-shared Shared tldraw.com types/logic OptimisticAppStore, license helpers
@tldraw/worker-shared Shared Cloudflare worker code bookmark unfurling, asset upload handlers, health
@tldraw/namespaced-tldraw Namespaced build of the SDK —
create-tldraw Project scaffolder CLI npx create-tldraw

Dependency direction

state → store → tlschema → editor → tldraw
           ↘ validate ↗
sync-core → sync         (both depend on store/tlschema)
worker-shared, dotcom-shared → consumed by apps/*

The chain is strictly layered: state knows nothing of store; store knows nothing of shapes; tlschema defines the shapes; editor orchestrates; tldraw adds the batteries-included defaults and UI. Each higher layer re-exports the layer below through barrel index.ts files, which is why import { Vec, Editor, Tldraw } from 'tldraw' works for everything.

Note

Because the public surface is a barrel of re-exports, static analysis tools (and AST extractors) see "zero functions" in the top package — the symbols are re-exports, not definitions. The authoritative API surface is each package's src/index.ts, and the type signatures (*.d.ts) generated by API Extractor.


4. The Reactive Core: Signals → Store → Schema

@tldraw/state — the signals library

The foundation is a fine-grained reactivity system, conceptually similar to MobX/Solid signals but purpose-built. Core primitives:

Primitive Class / fn Role
Atom __Atom__ / atom() A mutable reactive value
Computed __UNSAFE__Computed / computed() A derived value, lazily recomputed and cached
Effect __EffectScheduler__ Re-runs side effects when dependencies change
Transaction Transaction / transact() Batches mutations; a single notification at commit
History HistoryBuffer, WithDiff Tracks diffs for time-travel/undo
Dependency capture CaptureStackFrame, ArraySet Tracks which signals a computation read

@tldraw/state-react binds this to React: useValue(name, fn, deps) subscribes a component to exactly the signals fn reads, and track() wraps a component so it re-renders on any signal it consumes. This is how tldraw re-renders only the shapes that actually changed.

@tldraw/store — the reactive record store

The Store (packages/store/src/lib/Store.ts) is a reactive, in-memory database of records. Each record has a typeName and a typed id (shape:abc, binding:xyz). Key classes:

Class Role
Store The record store; CRUD, listeners, snapshots
RecordType Defines a record type (id prefix, validator, default props)
StoreSchema The full schema; runs migrations and validation
StoreQueries Reactive indexes/queries over records
StoreSideEffects before/after hooks for create/update/delete
HistoryAccumulator Accumulates change diffs between flushes
AtomMap, ImmutableMap Reactive and persistent map structures (HAMT-based)

Mutations are reactive: a query like "all shapes on the current page" is a computed that updates incrementally. Listeners receive diffs (added/updated/removed) scoped to document, session, or presence.

@tldraw/tlschema — the document schema

tlschema defines the concrete record types of a tldraw document: shapes (geo, arrow, draw, …), bindings, assets, pages, instance, camera, pointer, instance_page_state, and presence records — plus the style properties (color, size, fill, dash, font) and the migration sequences that evolve documents across versions. When you add a custom shape, you extend this schema with your record type and its migrations.

@tldraw/validate — runtime validation

A small validator combinator library (Validator, ArrayOfValidator, ObjectValidator, UnionValidator, DictValidator). Every record is validated on the way into the store, which is why loading a malformed snapshot throws a ValidationError with a precise message (e.g. Expected number, got string). Validation is the first line of defense against corrupt or malicious documents.

Important

The signals → store → schema stack is the conceptual heart of tldraw. Undo/redo is a history of store diffs; multiplayer is store diffs over a wire; persistence is a serialized store; the canvas is a reactive projection of the store. Understand this stack and the rest of the system follows.


5. The Editor Runtime

The Editor class (packages/editor/src/lib/editor/Editor.ts) is the central orchestrator. It owns the store, the current tool state, the camera, and a set of managers that each own one concern. Construction options are TLEditorOptions.

Managers

Manager Responsibility
HistoryManager Undo/redo stack, marks, batching
SnapManager (+ BoundsSnaps, HandleSnaps) Snapping to shapes, guides, handles
ClickManager Click vs double/triple click disambiguation
InputsManager Pointer/keyboard input state
TextManager Text measurement and layout (batched)
FontManager Font loading and readiness
ThemeManager Light/dark/custom themes, color resolution
TickManager The animation/render frame loop
ScribbleManager Laser/eraser scribble trails
EdgeScrollManager Auto-scroll near viewport edges
SpatialIndexManager Spatial index for hit-testing/culling
CollaboratorsManager Remote collaborator presence/cursors
PerformanceManager (+ PerformanceApiAdapter) Perf instrumentation events
UserPreferencesManager Per-user preferences (locale, color scheme, animation)

Tools as a state chart

Tools are nodes in a hierarchical state machine. StateNode is the base; the active tool dispatches editor events (onPointerDown, onKeyUp, onEnter, …) to its current child state. The default SelectTool, for example, has child states for idle, pointing, dragging, brushing, cropping, translating, resizing, and rotating. This makes complex interactions composable and testable. Custom tools extend StateNode (or BaseBoxShapeTool for drag-to-create).

Geometry primitives

The editor ships a complete 2D geometry kit used for hit-testing, bounds, and snapping: Vec, Box, Mat (matrix), and a Geometry2d hierarchy (Rectangle2d, Circle2d, Ellipse2d, Polygon2d, Polyline2d, Arc2d, CubicBezier2d, CubicSpline2d, Stadium2d, Group2d), plus intersection routines and math helpers. Custom shapes return a Geometry2d from getGeometry() to participate fully in selection, snapping, and binding.

The canvas renderer

<DefaultCanvas> renders shapes inside an <HTMLContainer>/<SVGContainer> coordinate space transformed by the camera. Rendering is driven by reactive subscriptions: only shapes whose records changed re-render. Off-screen shapes are culled (not rendered), and detail is reduced at low zoom (textShadowLod, debounced zoom) for performance.


6. Extension Model Internals

tldraw's extensibility is uniform: every extensible concept is a "util" class registered through a component prop and merged with the defaults.

Extension Base class Registered via Merge key
Shape ShapeUtil / BaseBoxShapeUtil shapeUtils type
Binding BindingUtil bindingUtils type
Tool StateNode / BaseBoxShapeTool tools id
Asset AssetUtil assetUtils type
Overlay OverlayUtil overlayUtils type
UI component React component components by key
Action/tool/translation object overrides by key

Merging uses mergeArraysAndReplaceDefaults(key, custom, defaults): a custom util with the same type/id as a default replaces it; otherwise it is added. This is why you pass only your custom utils — the defaults remain unless you deliberately override them.

ShapeUtil contract

A shape util defines:

  • static type — the shape's type string.
  • getDefaultProps() — initial props.
  • getGeometry(shape) — a Geometry2d for hit-testing/bounds/snapping.
  • component(shape) — the live React rendering (wrap in HTMLContainer/SVGContainer).
  • indicator(shape) — the selection outline (SVG).
  • Optional behavior hooks: onResize, onTranslate, canBind, canCrop, onEditEnd, toSvg (export), drag-in/out handlers, and more (see the TLResizeInfo, TLCropInfo, TLHandleDragInfo types).
  • Optional static props/migrations to register the record type and its schema evolution.

BindingUtil contract

Bindings model relationships. The util responds to lifecycle hooks — onAfterCreate, onAfterChange, onBeforeDelete, and shape-relative hooks (onAfterChangeFromShape, onBeforeIsolateFromShape, onBeforeDeleteFromShape) — to keep the two related shapes consistent. The built-in ArrowBindingUtil keeps an arrow's terminal attached to its target; isolation hooks decide what happens when one side is copied or deleted without the other.

Side effects

editor.sideEffects (backed by StoreSideEffects) lets you register registerBefore*/registerAfter* handlers for create/update/delete of any record type — the supported way to enforce invariants (validate before create, cascade deletes, derive shapes). registerDefaultSideEffects(editor) shows the canonical usage and is registered by <Tldraw> on mount.


7. Persistence & Document Model

A tldraw document is the serialized contents of the store. Three persistence strategies, all over the same store:

  1. Local (IndexedDB) — persistenceKey activates LocalIndexedDb (@tldraw/editor), which writes the document to the browser's IndexedDB and broadcasts changes across tabs. Each key is one document; the Table/StoreName types define the object stores used.
  2. Snapshots — getSnapshot(store) / loadSnapshot(store, snapshot) move a document (and optional session state) in and out as plain JSON. Use for server persistence: listen to editor.store and POST snapshots.
  3. .tldr files — serializeTldrawJson / parseTldrawJsonFile produce and consume the portable file format (TLDRAW_FILE_EXTENSION = '.tldr'). buildFromV1Document migrates legacy v1 files.

Session state (camera, current page, selection) is tracked separately from document state (TLSessionStateSnapshot, createSessionStateSnapshotSignal) so that per-tab view state doesn't pollute the shared document. TAB_ID scopes session state to a browser tab.

Warning

Migrations are central to durable persistence. Any custom record type must ship a MigrationSequence so that old documents load correctly after your schema changes. Loading a snapshot whose records reference unregistered types or fail validation throws ValidationError/Missing definition for record type ….


8. Multiplayer Architecture (@tldraw/sync)

Multiplayer is a client/server sync protocol with an authoritative room on the server and optimistic clients.

Client side (@tldraw/sync + @tldraw/sync-core)

Component Role
useSync / useSyncDemo React hooks returning a TLStoreWithStatus
TLSyncClient The client protocol engine: applies optimistic local changes, sends/receives diffs, reconciles
ClientWebSocketAdapter WebSocket transport
ReconnectManager Automatic reconnection with backoff
JsonChunkAssembler Reassembles chunked large messages
RemoteTLStoreWithStatus Store status: loading → synced-remote → error

The client applies changes locally immediately (optimistic), sends them to the server, and reconciles against authoritative updates. Presence (cursors, selection, name/color) is a separate record scope synced continuously and excluded from document history.

Server side (@tldraw/sync-core)

Component Role
TLSocketRoom High-level room API: manage sessions, broadcast, persistence hooks
TLSyncRoom The authoritative document + diff reconciliation logic
ServerSocketAdapter Server WebSocket adapter
PresenceStore Per-room presence records
SQLiteSyncStorage / InMemorySyncStorage Pluggable room storage backends
DurableObjectSqliteSyncWrapper, NodeSqliteWrapper SQLite wrappers for Durable Objects vs Node
TLRemoteSyncError / TLSyncError Protocol error types

TLSocketRoom is the integration seam: you instantiate it per document, feed it socket connections, and provide load/save callbacks. It is transport- and host-agnostic — the same room logic runs on Cloudflare Durable Objects (SQLite-backed) or a plain Node server (with NodeSqliteWrapper/InMemorySyncStorage). The simple-server-example and socketio-server-example templates show Node hosts; the Cloudflare path is the production reference.


9. Self-Hosting a Sync Backend

The supported, production-grade path is Cloudflare Workers + Durable Objects, scaffolded by the Multiplayer starter kit:

npx create-tldraw@latest -t multiplayer

This produces a tldraw-sync-cloudflare project whose worker hosts a TldrawDurableObject (one Durable Object instance per room) built on TLSocketRoom. Architecture:

Browser (useSync) ──WebSocket──► Cloudflare Worker
                                     │  routes /connect/:roomId
                                     ▼
                          TldrawDurableObject (one per room)
                             ├── TLSocketRoom (sync-core)
                             ├── SQLite storage (Durable Object)
                             └── R2 bucket for assets (images/video)

Why Durable Objects: each room maps to exactly one strongly-consistent, stateful object that serializes all access — the natural unit for a collaborative document. SQLite-in-DO gives durable per-room storage without a separate database.

Asset handling

Production multiplayer must offload media to blob storage. useSync({ assets }) takes an asset store; the Cloudflare kit backs it with an R2 bucket and the worker-shared asset-upload handlers (PUT /assets/:objectName, GET /assets/:objectName). Without an asset store, media is base64-inlined into the document and destroys serialization performance.

Generic host

For non-Cloudflare hosts, run TLSocketRoom on any Node WebSocket server (ws, socket.io). Provide:

  • A WebSocket endpoint per room.
  • onDataChange persistence to your datastore (Postgres, S3-backed snapshots, etc.).
  • An asset upload/serve endpoint.
  • Authentication on the upgrade request (see Security).

Note

useSyncDemo points at tldraw's hosted demo (bemo-worker, env TLDRAW_BEMO_URL/TLDRAW_IMAGE_URL). It is for prototypes only — rooms are public, asset upload is disabled, and persistence is best-effort.


10. The tldraw.com Reference Architecture

apps/dotcom is the production whiteboard at tldraw.com — the most complete reference for an enterprise-grade deployment. It runs entirely on Cloudflare.

Workers & Durable Objects (apps/dotcom/sync-worker)

Durable Object / class Role
TLDrawDurableObject / TLFileDurableObject One per document/file; hosts the TLSocketRoom, persists the room
TLUserDurableObject Per-user state and fan-out (which files a user has)
TLPostgresReplicator (+ SafeWal2JsonPlugin) Streams Postgres WAL (logical replication via wal2json) into the sync layer to keep app metadata in sync
TLLoggerDurableObject Centralized logging DO
TLStatsDurableObject Stats/metrics aggregation
AlarmScheduler Durable Object alarm scheduling (timeouts, cleanup)
UserDataSyncer Reconciles user data between Postgres and DOs
TLPostgresPool / LoggingClient Postgres connection pooling with logging

Supporting workers (apps/dotcom/*)

Worker Role
asset-upload-worker Receives user asset uploads → R2
image-resize-worker On-the-fly image resizing/optimization
tldrawusercontent-worker Serves user-generated content from a separate origin (isolation)
zero-cache Runs Zero sync over Postgres for app/file metadata (sidebar, sharing, permissions)

Client (apps/dotcom/client)

A Vite React SPA (TldrawApp, ClientCRUD, OptimisticAppStore, a Zero polyfill) wrapping the SDK with file management, auth (Clerk), sharing, and the "tldraw assistant" (tla) account layer. The canvas itself is the tldraw SDK; everything around it is app shell.

Browser SPA (client) ──┬── /connect/:fileId  → sync-worker → TLFileDurableObject
                       ├── /assets           → asset-upload-worker → R2
                       ├── images            → image-resize-worker
                       ├── app metadata      → zero-cache → Postgres
                       └── usercontent       → tldrawusercontent-worker (isolated origin)
                                  ▲
                       TLPostgresReplicator (WAL → DOs)

Important

Two distinct sync systems coexist in tldraw.com: document sync (the canvas, via TLSocketRoom/Durable Objects) and app-data sync (file lists, sharing, presence-in-app, via Zero over Postgres). Do not conflate them — the SDK's @tldraw/sync is only the document-sync half.


11. Other Products in the Repo

Product Path Summary
VS Code extension apps/vscode A custom editor for .tldr files. TLDrawDocument + TldrawEditorProvider register a custom editor; TldrawWebviewManager/WebViewMessageHandler host the SDK in a webview and bridge file save/load messages
MCP server apps/mcp-app TldrawMCP (extends McpAgent) exposes tldraw as a Model Context Protocol tool, letting LLM agents manipulate a canvas
Docs site apps/docs Next.js site for tldraw.dev; builds the API reference from API Extractor output (TldrawApiModel), Algolia search, analytics
Examples app apps/examples The dev playground (localhost:5420); hundreds of runnable examples — the de-facto integration test surface and documentation-by-example
Analytics apps/analytics, apps/analytics-worker First-party analytics collection
Health worker internal/health-worker Uptime/health checks
Bemo worker apps/bemo-worker The public demo sync server (BemoDO) behind useSyncDemo
Templates templates/* Eight starter kits (basic, multiplayer, agent, workflow, chat, image-pipeline, branching-chat, shader) plus server examples

12. Configuration & Environment Reference

SDK runtime options

The SDK's behavioral configuration is the TldrawOptions object passed via the options prop — see the User Guide § Configuration Options Reference for the complete table with defaults (document limits, timing, camera, snapping, collaboration timing, clipboard hooks, etc.). Defaults live in defaultTldrawOptions (packages/editor/src/lib/options.ts). There is no environment-variable configuration in the SDK itself — it is a browser library.

Environment variables (apps, not the SDK)

These are consumed by the deployed apps and build tooling, not by the published package. They are listed here so operators of a tldraw.com-style deployment know what to set.

Variable Consumed by Purpose
TLDRAW_LICENSE @tldraw/dotcom-shared License key for the hosted app
USER_CONTENT_URL dotcom client Base URL for user content origin (required)
MULTIPLAYER_SERVER dotcom client, image-resize-worker Sync server URL (required)
ZERO_SERVER dotcom client Zero (app-metadata sync) server URL (required)
TLDRAW_ENV dotcom client Deployment environment (required; must be set)
SENTRY_DSN / SENTRY_CSP_REPORT_URI dotcom client Error reporting / CSP report endpoint
BOTCOM_POSTGRES_POOLED_CONNECTION_STRING zero-cache migrate Postgres connection for app metadata
TLDRAW_BEMO_URL, TLDRAW_IMAGE_URL @tldraw/sync demo Demo sync + image server endpoints
NEXT_PUBLIC_ALGOLIA_*, NEXT_PUBLIC_GA4_*, NEXT_PUBLIC_VERCEL_* docs site Search, analytics, Vercel build metadata
CI, GITHUB_REF, PR_NUMBER, TLDRAW_PREVIEW_ID, ALLOW_REFRESH_ASSETS_CHANGES build/deploy scripts CI and deploy plumbing

Caution

The dotcom client fails fast if USER_CONTENT_URL, MULTIPLAYER_SERVER, ZERO_SERVER, or TLDRAW_ENV are missing (Missing … env var / TLDRAW_ENV must be set). Treat these as required configuration for any self-hosted clone of tldraw.com; they have no safe defaults.

Debug & feature flags

The editor exposes debugFlags and featureFlags (@tldraw/editor), surfaced in the Debug menu. These are developer toggles (force mobile, show culling, performance overlays) — not production configuration. They are read from URL/localStorage in development.


13. Build, Release & Versioning

Toolchain

  • Package manager: yarn via corepack (run npm i -g corepack then yarn). Node ^20.0.0.
  • Task runner: lazyrepo (caching task orchestration across the workspace).
  • Per-package build: internal/scripts/build-package.ts (invoked by each package's build).
  • API surface: Microsoft API Extractor (api-extractor.json per package, extending internal/config/api-extractor.json) produces rolled-up .d.ts and the API model the docs site consumes. build-api.ts drives this.
  • Tests: Vitest (jsdom preset for DOM, node preset for logic), with extensive polyfills (fake-indexeddb, resize-observer-polyfill, webcrypto, FontFace mocks) for the DOM-heavy editor. Playwright drives e2e in apps/*/e2e.
  • Lint: custom internal/scripts/lint.ts plus a tldraw/* ESLint rule set (e.g. tldraw/no-export-star is suppressed only at the barrel boundaries).

Local development

npm i -g corepack   # enable corepack for the pinned yarn version
yarn                # install workspace deps
yarn dev            # runs the examples app at localhost:5420

Versioning

Packages are versioned in lockstep — tldraw, @tldraw/editor, and the rest publish together at the same version (currently 5.1.0; major published 2026-05-06, minor/patch 2026-06-03). Each package's version.ts is generated by refresh-assets.ts. registerTldrawLibraryVersion(...) runs at import time to detect duplicate/mismatched copies in a bundle.

Warning

Mixing versions of @tldraw/* packages is unsupported and triggers a runtime warning. Always let the top-level tldraw package pull in matching scoped versions; pin a single tldraw version in your app and deduplicate React.

Deploy

The apps deploy to Cloudflare (workers/DOs via Wrangler — see internal/scripts/lib/deploy.ts, which parses the deploy id/worker name from wrangler output) and Vercel (docs/examples). The deploy scripts compute the commit SHA and a preview id; failures like Could not determine the SHA of the commit to deploy indicate a detached/shallow checkout.


14. Security Analysis

tldraw is a client library; its security surface is dominated by (a) the SDK's handling of untrusted document/clipboard content and (b) the infrastructure you build around multiplayer and asset hosting.

SDK (client) surface

Area Posture
Document validation Every record entering the store is validated by @tldraw/validate. Malformed/hostile documents throw ValidationError rather than corrupting state. This is the primary defense against malicious .tldr/snapshot input.
SVG/HTML import External SVG content is sanitized (sanitizeSvg) before rendering. Custom shapes that render untrusted HTML are your responsibility — sanitize before injecting.
Embeds The embed shape only allows a curated allow-list of providers with per-provider permission defaults (embedShapePermissionDefaults); unknown embeds get restrictive overrides.
CSP The options.nonce lets you supply a nonce for tldraw's inline styles under a strict CSP.
Clipboard/drop hooks onBeforePasteFromClipboard, onClipboardPasteRaw, onBeforeCopyToClipboard, and experimental__onDropOnCanvas let you filter or block untrusted clipboard/drag content before it becomes shapes.

Infrastructure (multiplayer) surface

Area Guidance
Authentication The sync protocol does not authenticate for you. Authenticate the WebSocket upgrade (and asset upload requests) at your worker/server. tldraw.com uses Clerk; a self-host must add its own authz on TLSocketRoom connections.
Authorization Enforce per-room/per-file access on connect and on asset read/write. Document sync gives anyone with a room connection full edit rights unless you gate it.
Asset isolation tldraw.com serves user content from a separate origin (tldrawusercontent-worker) to prevent user-uploaded HTML/SVG from running with the app's privileges. Mirror this — never serve user uploads from your app origin.
Asset validation The worker-shared upload handlers and bookmark unfurler validate content type and reject non-images where appropriate (Not an image, No content type). Bound asset size with maxAssetSize.
Secrets All secrets live in env (Postgres connection strings, license keys, Sentry DSNs). None are committed. Rotate the license key and DB credentials through your platform's secret store.
SSRF (bookmark unfurling) The bookmark fetcher (/api/bookmark) fetches arbitrary URLs to build link previews — a classic SSRF vector. Run it in an isolated worker with egress controls; do not let it reach internal services.

Caution

The biggest self-host security mistake is treating the sync server as if it authenticates and authorizes. It does not. You must add auth on the WebSocket upgrade, authz per room, asset-origin isolation, and SSRF controls on bookmark unfurling. The tldraw.com architecture demonstrates each of these — copy the pattern, not just the canvas.

Malicious-content note

This repository is a legitimate, widely used open-source SDK. No malware, credential harvesting, or covert exfiltration was observed. The only outbound calls of note are anonymous, opt-out-able telemetry in create-tldraw (which kit was chosen, to dashboard.tldraw.pro/teamldraw.com, disabled by --no-telemetry) and the SDK's CDN fetches for fonts/translations.


15. Performance & Scaling

Client rendering

tldraw's DOM rendering is made fast by several mechanisms you should understand and respect:

Technique Mechanism Operator note
Fine-grained reactivity Only shapes whose records change re-render Don't pass unstable props that force full rebuilds
Culling Off-screen shapes are not rendered Keep getGeometry accurate so culling is correct
Spatial index SpatialIndexManager accelerates hit-testing Custom shapes get this for free via geometry
LOD Text shadows dropped below textShadowLod (0.35); debounced zoom above debouncedZoomThreshold (500 shapes) Tune via options for very dense canvases
Batched mutations editor.run() / transactions collapse N changes into one render Wrap bulk programmatic edits in editor.run
Text/font batching TextManager/FontManager batch measurement and gate render on at most maxFontsToLoadBeforeRender fonts —

Document scale defaults: maxShapesPerPage = 4000, maxPages = 40. These are guardrails, not hard ceilings — raise them deliberately and test, since rendering cost grows with on-screen shape count.

Multiplayer scaling

  • Per-room isolation: one Durable Object per document means rooms scale horizontally and independently; a hot room doesn't affect others.
  • Optimistic clients: local edits never wait on the network; latency only affects convergence, not responsiveness.
  • Presence cost: presence is high-frequency but small and ephemeral (separate scope, not persisted/historied).
  • Asset offload: external blob storage (R2/S3) keeps documents small and sync messages cheap. Inline base64 is the #1 multiplayer performance killer.
  • Chunking: JsonChunkAssembler handles large initial-load messages without blocking.

Tip

For very large canvases, profile with the Performance menu / PerformanceManager events, raise debouncedZoomThreshold, and consider a custom LOD strategy in your shape's component() that renders simplified content at low zoom.


16. Operations Runbook

These procedures apply to a self-hosted, tldraw.com-style deployment (the SDK itself needs no ops).

Deploy the SDK in your app

npm i tldraw        # pin a single version
# build your app; ensure one copy of tldraw + react in the bundle

Stand up multiplayer (Cloudflare)

npx create-tldraw@latest -t multiplayer my-sync-app
cd my-sync-app && npm install
# configure wrangler.toml (DO bindings, R2 bucket), then:
npm run deploy      # wrangler deploy

Run app-metadata migrations (zero-cache, dotcom-style)

# requires BOTCOM_POSTGRES_POOLED_CONNECTION_STRING
node apps/dotcom/zero-cache/migrate.ts

Failure modes: Missing BOTCOM_POSTGRES_POOLED_CONNECTION_STRING env var, Failed to connect to postgres, Migrations path not found, No migrations found, Previously-applied migration … not found (history drift — never delete applied migration files).

Health checks

internal/health-worker and the worker-shared GET /health endpoint provide liveness. Wire these to your uptime monitor.

Backup / restore

Document state lives in each room's Durable Object SQLite store (or your configured datastore). Snapshot rooms via TLSocketRoom persistence callbacks; back up Postgres (app metadata) and R2 (assets) on your platform's schedule. To restore a single document, load its snapshot into a fresh room.

Common incidents

Symptom Likely cause Action
Clients stuck on loading Room DO not reachable / auth rejecting upgrade Check worker logs, auth on upgrade, DO binding
error status on connect Bad uri, version mismatch, server TLRemoteSyncError Verify URL, align SDK versions client/server
Huge documents / slow sync Inline base64 assets Add an asset store + R2; migrate inline assets out
Asset 4xx on upload Content-type/size rejected Check maxAssetSize, accepted MIME types, R2 perms
User content executing scripts Assets served from app origin Move to an isolated content origin

17. Code Quality & Conventions

  • TypeScript everywhere, fully typed public API. The .d.ts produced by API Extractor is the contract; @public/@internal TSDoc tags gate what is exported.
  • Barrel exports at each package's index.ts define the public surface; tldraw/no-export-star is enforced except at deliberate re-export boundaries.
  • One class, one concern. The Editor delegates to single-responsibility managers; tools are small state nodes; shapes are self-contained utils. This keeps the 6 MB codebase navigable.
  • Reactivity over imperative wiring. Derived state is computed; components subscribe via useValue/track. Avoid manual cache invalidation.
  • Validation at boundaries. Records are validated entering the store; clipboard/drop content passes through filter hooks; assets validate content type.
  • Heavy test polyfilling for the DOM-dependent editor (fake IndexedDB, ResizeObserver, FontFace, webcrypto, getComputedStyle) so logic can run under jsdom.
  • Generated files are marked (This file is automatically generated …) — version.ts, templates.ts, asset manifests — and must not be hand-edited.

18. Potential Improvements

These are observations for maintainers and integrators, derived from the source structure (not defects):

  • Framework breadth. The core is framework-agnostic but the published bindings are React-only (plus a Vue template). First-class Vue/Svelte/Solid bindings would broaden adoption; the signals layer already supports it conceptually.
  • Deprecation cleanup. Several <Tldraw> props are deprecated in favor of options.* (cameraOptions→options.camera, textOptions→options.text, embeds→EmbedShapeUtil.configure). A future major could remove them to shrink the prop surface.
  • Self-host auth scaffolding. The Multiplayer starter kit ships an unauthenticated room by default. A starter that bundles a minimal auth/authz example would reduce the most common security mistake (see § Security).
  • Open feature requests (from the issue tracker) that shape the roadmap: first-class LOD abstraction for shape rendering at zoom (#8307), waitForSync on the sync client (#7893), resizeToBounds on the editor (closed/landed), and richer arrow-to-group binding (#8342).
  • Known interaction bugs to track for app developers: pen tool unresponsive after zoom (#7787), curved-arrow drift on undo after deleting a bound shape (#8203), arrow binding leaving a geo shape after a geo-type change (#9030), and pinch-gesture selection changes on touch (#8397). If your app stresses arrows/bindings or touch, validate against these.

19. Quick Reference

Package & versions

Item Value
Flagship package tldraw 5.1.0
Engine @tldraw/editor 5.1.0 (lockstep)
React peer ^18.2.0 || ^19.2.1
Node (repo dev) ^20.0.0
License tldraw license (production key required)
Source commit 5e29f4e (2026-06-04)

Key classes by layer

Layer Classes
Signals __Atom__, __UNSAFE__Computed, Transaction, EffectScheduler, HistoryBuffer
Store Store, RecordType, StoreSchema, StoreQueries, StoreSideEffects, AtomMap
Validate Validator, ArrayOfValidator, ObjectValidator, UnionValidator, DictValidator
Editor Editor, ShapeUtil, BindingUtil, StateNode, BaseBoxShapeUtil, *Manager
Geometry Vec, Box, Mat, Geometry2d (+ Rectangle2d, Circle2d, Polygon2d, …)
Sync client TLSyncClient, ClientWebSocketAdapter, ReconnectManager, JsonChunkAssembler
Sync server TLSocketRoom, TLSyncRoom, ServerSocketAdapter, SQLiteSyncStorage, PresenceStore
dotcom DOs TLFileDurableObject, TLUserDurableObject, TLPostgresReplicator, TLStatsDurableObject

Worker HTTP endpoints (worker-shared)

Method Path Purpose
GET/POST /api/bookmark Bookmark/link unfurling (SSRF-sensitive)
PUT/GET /assets/:objectName User asset upload / serve
GET /health Health check

Self-host checklist

  • [ ] Pin one tldraw version; dedupe React.
  • [ ] Provide a production licenseKey.
  • [ ] Run TLSocketRoom per document (Cloudflare DO or Node).
  • [ ] Authenticate the WebSocket upgrade; authorize per room.
  • [ ] Back media with R2/S3 via an assets store (no inline base64).
  • [ ] Serve user content from an isolated origin.
  • [ ] Constrain bookmark unfurling egress (SSRF).
  • [ ] Set required env (USER_CONTENT_URL, MULTIPLAYER_SERVER, ZERO_SERVER, TLDRAW_ENV).
  • [ ] Wire /health to uptime monitoring; back up Postgres + room storage.

Further reading