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
- Executive Summary
- System Architecture
- The Monorepo & Package Graph
- The Reactive Core: Signals → Store → Schema
- The Editor Runtime
- Extension Model Internals
- Persistence & Document Model
- Multiplayer Architecture (
@tldraw/sync) - Self-Hosting a Sync Backend
- The tldraw.com Reference Architecture
- Other Products in the Repo
- Configuration & Environment Reference
- Build, Release & Versioning
- Security Analysis
- Performance & Scaling
- Operations Runbook
- Code Quality & Conventions
- Potential Improvements
- 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'stypestring.getDefaultProps()— initial props.getGeometry(shape)— aGeometry2dfor hit-testing/bounds/snapping.component(shape)— the live React rendering (wrap inHTMLContainer/SVGContainer).indicator(shape)— the selection outline (SVG).- Optional behavior hooks:
onResize,onTranslate,canBind,canCrop,onEditEnd,toSvg(export), drag-in/out handlers, and more (see theTLResizeInfo,TLCropInfo,TLHandleDragInfotypes). - Optional
static props/migrationsto 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:
- Local (IndexedDB) —
persistenceKeyactivatesLocalIndexedDb(@tldraw/editor), which writes the document to the browser's IndexedDB and broadcasts changes across tabs. Each key is one document; theTable/StoreNametypes define the object stores used. - Snapshots —
getSnapshot(store)/loadSnapshot(store, snapshot)move a document (and optional session state) in and out as plain JSON. Use for server persistence: listen toeditor.storeand POST snapshots. .tldrfiles —serializeTldrawJson/parseTldrawJsonFileproduce and consume the portable file format (TLDRAW_FILE_EXTENSION = '.tldr').buildFromV1Documentmigrates 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
MigrationSequenceso that old documents load correctly after your schema changes. Loading a snapshot whose records reference unregistered types or fail validation throwsValidationError/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.
onDataChangepersistence to your datastore (Postgres, S3-backed snapshots, etc.).- An asset upload/serve endpoint.
- Authentication on the upgrade request (see Security).
Note
useSyncDemopoints at tldraw's hosted demo (bemo-worker, envTLDRAW_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/syncis 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, orTLDRAW_ENVare 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 corepackthenyarn). 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'sbuild). - API surface: Microsoft API Extractor (
api-extractor.jsonper package, extendinginternal/config/api-extractor.json) produces rolled-up.d.tsand the API model the docs site consumes.build-api.tsdrives 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 inapps/*/e2e. - Lint: custom
internal/scripts/lint.tsplus atldraw/*ESLint rule set (e.g.tldraw/no-export-staris 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-leveltldrawpackage pull in matching scoped versions; pin a singletldrawversion 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:
JsonChunkAssemblerhandles large initial-load messages without blocking.
Tip
For very large canvases, profile with the Performance menu /
PerformanceManagerevents, raisedebouncedZoomThreshold, and consider a custom LOD strategy in your shape'scomponent()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.tsproduced by API Extractor is the contract;@public/@internalTSDoc tags gate what is exported. - Barrel exports at each package's
index.tsdefine the public surface;tldraw/no-export-staris 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 viauseValue/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 ofoptions.*(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),
waitForSyncon the sync client (#7893),resizeToBoundson 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
tldrawversion; dedupe React. - [ ] Provide a production
licenseKey. - [ ] Run
TLSocketRoomper document (Cloudflare DO or Node). - [ ] Authenticate the WebSocket upgrade; authorize per room.
- [ ] Back media with R2/S3 via an
assetsstore (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
/healthto uptime monitoring; back up Postgres + room storage.
Further reading
- API reference & guides: tldraw.dev
- Sync docs: tldraw.dev/docs/sync
- Repository: github.com/tldraw/tldraw
- For installation, component props, and day-to-day usage, see the User Guide.