Course progress Course outline 18 of 18 lessons available
Part I: Choose the Surface Before You Draw—Product, Pixels, and Coordinates
Part II: Give the Pixel World a Brain—Model, Scheduling, Input, and Tools
- 05 Chapter 5: Give the Pixel World a Registry available now
- 06 Chapter 6: Redraw Only When the Light Turns On—Render Scheduling and the React Boundary available now
- 07 Chapter 7: Mouse, Touch, and Pen Speak One Language available now
- 08 Chapter 8: Find the Big Box Before Inspecting the Edge available now
- 09 Chapter 9: Tools Are Traffic Lights, Not a Bag of Booleans available now
Part III: From “It Drags” to “It Is Trustworthy”—Interaction, Text, Assets, and Recovery
- 10 Chapter 10: Make the Editor Feel Right available now
- 11 Chapter 11: Drawn Text Is Not Editable Text available now
- 12 Chapter 12: Borrowed Images Cannot Be Packed Without Rules available now
- 13 Chapter 13: Time Machines and Old Boxes available now
- 14 Chapter 14: Looking Correct Is Not Being Correct available now
Part IV: Master-Level Decisions—Performance, Workers, GPU, SDKs, Collaboration, and AI
- 15 Chapter 15: Do Not Search Ten Thousand Children One by One available now
- 16 Chapter 16: Keep the Front Desk Out of the Kitchen—Worker and GPU Upgrades available now
- 17 Chapter 17: Build the Car or Buy a Proven Chassis? Current lesson
- 18 Chapter 18: People and AI Edit the Same Ledger available now
Start with a game a five-year-old can understand
The one truth in this chapter: choosing a Library is a product, architecture, licensing, maintenance, and exit-cost decision—not a contest for the coolest Demo.
There are three ways to build a vehicle on the table. The first box contains only steel plates and screws: you build the steering wheel, brakes, and wheels yourself. It gives you the most freedom to make a Mars rover, but every broken screw belongs to you. The second box contains a complete school bus: the seats, seat belts, and doors already work, so it can carry people quickly, but you must follow the vendor’s dimensions and usage contract. The third approach buys a mature chassis while keeping control of the body, route signs, and passenger list.
Predict first: because the team learned to smelt steel, should it begin tomorrow’s school run by mining ore? No. Now predict whether changing vehicles will be easy if the school-bus vendor changes its connector next year and the passenger list exists only on the vendor dashboard. It will not. The real questions are: what must you own, what can you buy, how much does the contract cost, and how will you carry the passengers and routes away if it breaks or you leave?
- Write down the destinationDefine product capabilities first
- Compare chassisDo not compare only speed
- Read the usage contractBudget production licensing
- Install your own connectorDomain adaptation layer
- Rehearse changing vehiclesAll data can leave intact
This chapter evaluates exactly tldraw@5.3.2, never latest. Pinning a version makes code, migrations, license review, and regression baselines refer to the same thing. It does not mean never upgrading; it means every upgrade is an explicit project.
Translate the toys into Canvas
| Toy vehicle decision | Canvas Lab decision | Must answer |
|---|---|---|
| Build the whole vehicle from steel | Build / Raw Canvas | Will the team own the editor kernel permanently? |
| Buy a complete school bus | Buy / SDK | Do existing capabilities fit, and are licensing and upgrades acceptable? |
| Buy the chassis and build the body | Hybrid | Where is the boundary between SDK and Business Domain? |
| Passengers and routes | Domain Model | Long-lived facts owned by the business backend |
| Dashboard’s internal signals | SDK Store | Records required for Shapes, Selection, Tools, and rendering |
| Connector adapter | Domain Adapter | Bidirectional conversion, ID mapping, and fault isolation |
| Usage contract | License | Development, trial, commercial, and hobby production terms |
| Replacement door | Exit Strategy | Export, replacement Renderer, data repatriation, and rehearsal |
| Annual inspection | Upgrade Regression | Custom Shape/Tool/Binding/Migration regression |
Where the analogy stops: an SDK is not a completely closed school bus. tldraw’s source is visible and it has many extension points. “Buying” primarily means licensing and delegating maintenance responsibility to the SDK. An adaptation layer cannot magically remove all coupling either. Selection feel, clipboard format, collaboration protocol, and export pixels still create product dependencies and must appear in the exit checklist.
Kill the wrong intuitions first
- “tldraw contains ‘draw,’ so every Shape is drawn into a Canvas 2D bitmap.” In 5.3.2, a ShapeUtil’s
component()returns a React element. Primary Shapes use the React component layer and DOM/HTML/SVG. Selection outlines, brush, snap, and similar overlays may be drawn in Canvas 2D by OverlayUtil. It is not “everything is Canvas 2D.” - “tldraw sync is built-in Yjs.” False. tldraw sync uses an authoritative server, client pending/confirmed layers, and diff reconciliation. The official documentation explicitly says it is not CRDT/Yjs. Yjs is another backend option that can be integrated separately.
- “The source is on GitHub, so production is always free.” False. Version 5.3.2 is covered by the tldraw SDK license. Development needs no key. Production requires a valid trial, commercial, or hobby key. Commercial production requires a commercial license; hobby is for noncommercial use and retains a watermark.
- “Readonly means permission-secure.” Readonly is a client interaction mode. A malicious client can still call the network/API directly. The server must validate identity, object permissions, and every write.
- “Making business tables equal TLStore is the simplest solution.” SDK Records and the product domain have different versions, IDs, and lifecycles. Treating the Store as the only backend model binds exit and reporting to the SDK.
- “A 5.x upgrade can contain only patch-compatible changes.” tldraw officially states that it does not use SemVer; ordinary minor releases may contain breaking changes. Pin exact versions, read release notes, migrate, and regress.
- “The Library is popular, so Build/Buy has already been decided for us.” User tasks, object complexity, accessibility, licensing, collaboration, team capability, and exit cost are the inputs.
Production backpack
Prerequisites
Chapter 1 already defines the Renderer/product boundary. Chapter 15 has a real performance Trace. Chapter 16 proves whether a Worker/GPU is necessary. Without this evidence, “we may have 100,000 objects someday” cannot justify a complex technology. The Domain Document and Migrations from Chapters 5 and 13 remain long-term facts regardless of SDK choice.
Formal knowledge
Selection matrix. Raw Canvas 2D suits highly customized products, simple objects, unusual drawing algorithms, and teams willing to own the entire cost of Geometry, Selection, Accessibility, Text, History, Clipboard, Collaboration, and Migration. SVG/DOM suits products where text, semantics, Accessibility, CSS, and Layout matter, object counts are moderate, and native browser interaction is valuable. A Konva-style Scene Graph suits teams that want Canvas 2D without maintaining every Node, Event, and Transform from scratch. GPU 2D suits many Sprites, dynamic effects, high Fill Rate, and custom Shaders—after a Profile proves the GPU path helps. Three.js suits real Scene/Camera/Mesh/Material, Perspective, Depth, Lighting, and Raycasting, not the default upgrade for an ordinary 2D infinite canvas.
tldraw suits Whiteboards, Diagrams, Workflows, Infinite Canvas, and complex Selection, Tools, History, Text, Binding, Export, and Collaboration. In 5.3.2, primary Shape rendering is driven by a React component hierarchy, and ShapeUtil can return HTML/SVG. The new OverlayUtil system draws transient Selection/snapping UI directly into Canvas 2D. The accurate description is therefore “primarily React/DOM/SVG, with some overlays using Canvas 2D,” not either absolute slogan.
Pin dependencies. Use npm install --save-exact tldraw@5.3.2 @tldraw/sync@5.3.2, commit the lockfile, and make CI assert package versions. Never use ^5.3.2. Release the client and sync server together. Register Custom Shape/Binding schemas and migrations on both. Before changing the lockfile, open old Snapshots in an isolated compatibility environment, export golden documents, and run browser and visual regressions.
Production licensing. tldraw@5.3.2 is source-available, not permissive open source like MIT or Apache. Verify the official terms as of 2026-08-29: development requires no key; production requires a valid license key. The 100-day trial is for evaluation. Commercial products and internal enterprise production use commercial licensing. Noncommercial users may apply for a discretionary hobby license that carries a “made with tldraw” watermark. A key is validated locally in the client and may be public, but contains allowed hosts, type, and expiry. Price is a procurement input, so never guess a fixed number in the ADR. Record the sales quote, renewal, domains, expiry, and legal approval. Add license telemetry to privacy review as well. The current official License key page says commercial keys send no data, while trial/hobby send the license ID, SDK version, and page URL. That is not Canvas content, but a URL may still contain a sensitive path or query parameter. Test production domains and routes, and have legal/privacy owners confirm them. tldraw sync is covered by the SDK license, but that does not make it a free hosted service. The official project does not host production sync; the team must self-host and persist it.
Store is not Domain. The SDK owns the canvas representation of shapes/pages/bindings/assets, tool state, and internal migrations. The business backend owns WorkItem, permissions, approval state, audit, business version, Asset policy, and stable business ID. DomainAdapter.toCanvas projects a business object into a Shape. fromCanvasCommand converts an allowed edit into a business Command. Shape meta.domainId is only a reference; do not put salary, secrets, or the complete business Payload into Store.
Extension surface. A Custom Shape supplies a props validator, migration, geometry, React component, indicator, and resize behavior. A Custom Tool uses hierarchical StateNode. A Custom Binding expresses a directional relationship maintained by BindingUtil. A Custom Record must declare its document/session/presence scope, validator, and migration. Domain Adapter is your anti-corruption layer. Visual Export uses the SDK’s SVG/PNG pipeline; semantic Export uses your own Domain JSON. Readonly Mode serves a Viewer, but Permission-Aware Controls must also hide or disable commands by capability, and the server must revalidate every write.
Evidence and compatibility (verified 2026-08-29)
Pin the npm Registry release tldraw 5.3.2 and verify the matching @tldraw/sync 5.3.2. See the official Shapes documentation for the React component hierarchy and Geometry contract, and Overlay utils for the Canvas 2D transient-layer exception. See Store and Persistence for Store scope, Custom Records, and Migrations. See Collaboration for tldraw sync’s authoritative-server model; the official project also states that tldraw sync is not Yjs/CRDT. For production keys, license types, and current data transmission, follow the more specific License key page and also read the full License. Legal counsel must still review the actual agreement before procurement; this tutorial is not legal advice.
Engineering increment for this chapter
Starting point: Canvas Lab’s custom kernel provides complete intellectual ownership and teaching value, but Whiteboard-grade interactions are expensive to deliver. Finish line: produce a Hybrid spike pinned to 5.3.2 that rebuilds a business Card Shape, creation Tool, dependency Binding, Custom Record, Domain Adapter, Migration, dual Export, Readonly, and Permission controls—and write an ADR with an executable exit plan.
Add these files and interfaces:
src/sdk/tldraw/domain.tsandDomainAdapter.ts: the sole business boundary;src/sdk/tldraw/TaskShapeUtil.tsx: Custom Shape + props migration;src/sdk/tldraw/CreateTaskTool.ts: Custom Tool;src/sdk/tldraw/DependencyBindingUtil.ts: Custom Binding;src/sdk/tldraw/schema.ts: document-scoped Custom Record;src/sdk/tldraw/CanvasSdkView.tsx: license, readonly, and permission controls;src/sdk/tldraw/export.ts: Domain JSON and visual export;docs/adr/017-canvas-sdk.md: Build/Buy/Hybrid decision and exit rehearsal.
Below is a runnable Custom Shape, Migration, Tool, Domain Adapter, and permission shell. It deliberately keeps the business Task free of tldraw imports:
import { useMemo } from 'react';
import {
BaseBoxShapeUtil,
HTMLContainer,
Rectangle2d,
StateNode,
T,
Tldraw,
TLBaseShape,
TLCreateShapePartial,
TLShapeId,
createShapeId,
createShapePropsMigrationIds,
createShapePropsMigrationSequence,
exportAs,
resizeBox,
} from 'tldraw';
import 'tldraw/tldraw.css';
import { createCanvasStore } from './schema';
export type Task = Readonly<{
id: string;
title: string;
status: 'todo' | 'doing' | 'done';
revision: number;
}>;
type TaskShape = TLBaseShape<
'task-card',
{ w: number; h: number; domainId: string; title: string; status: Task['status'] }
>;
const versions = createShapePropsMigrationIds('task-card', { AddStatus: 1 });
const migrations = createShapePropsMigrationSequence({
sequence: [
{
id: versions.AddStatus,
up: (props: Record<string, unknown>) => {
props.status ??= 'todo';
},
down: (props: Record<string, unknown>) => {
delete props.status;
},
},
],
});
export class TaskShapeUtil extends BaseBoxShapeUtil<TaskShape> {
static override type = 'task-card' as const;
static override props = {
w: T.number,
h: T.number,
domainId: T.string,
title: T.string,
status: T.literalEnum('todo', 'doing', 'done'),
};
static override migrations = migrations;
override getDefaultProps(): TaskShape['props'] {
return { w: 240, h: 112, domainId: '', title: 'New task', status: 'todo' };
}
override getGeometry(shape: TaskShape) {
return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true });
}
override component(shape: TaskShape) {
return (
<HTMLContainer
style={{
pointerEvents: 'all',
border: '2px solid #334155',
borderRadius: 12,
background: '#fff',
padding: 12,
}}
>
<strong>{shape.props.title}</strong>
<p>{shape.props.status}</p>
</HTMLContainer>
);
}
override getIndicatorPath(shape: TaskShape) {
const path = new Path2D();
path.rect(0, 0, shape.props.w, shape.props.h);
return path;
}
override onResize(shape: TaskShape, info: Parameters<typeof resizeBox>[1]) {
return resizeBox(shape, info);
}
}
export const DomainAdapter = {
toShape(
task: Task,
existingId?: TLShapeId,
): TLCreateShapePartial<TaskShape> & { id: TLShapeId; props: TaskShape['props'] } {
return {
id: existingId ?? createShapeId(task.id),
type: 'task-card',
x: 0,
y: 0,
props: { w: 240, h: 112, domainId: task.id, title: task.title, status: task.status },
};
},
toDomainPatch(
shape: Pick<TaskShape, 'props'>,
before: Task,
): Pick<Task, 'id' | 'title' | 'status' | 'revision'> {
if (shape.props.domainId !== before.id) throw new Error('DOMAIN_ID_MISMATCH');
return {
id: before.id,
title: shape.props.title,
status: shape.props.status,
revision: before.revision,
};
},
};
export class CreateTaskTool extends StateNode {
static override id = 'create-task';
override onEnter() {
this.editor.setCursor({ type: 'cross', rotation: 0 });
}
override onPointerDown() {
const p = this.editor.inputs.getCurrentPagePoint();
const id = createShapeId();
this.editor.createShape<TaskShape>({ id, type: 'task-card', x: p.x, y: p.y });
this.editor.select(id);
if (!this.editor.getInstanceState().isToolLocked) this.editor.setCurrentTool('select');
}
override onExit() {
this.editor.setCursor({ type: 'default', rotation: 0 });
}
}
type Capabilities = Readonly<{ edit: boolean; export: boolean }>;
const tools = [CreateTaskTool];
export function CanvasSdkView({
capabilities,
licenseKey,
}: {
capabilities: Capabilities;
licenseKey: string;
}) {
const store = useMemo(createCanvasStore, []);
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
licenseKey={licenseKey}
store={store}
tools={tools}
onMount={(editor) => {
editor.updateInstanceState({ isReadonly: !capabilities.edit });
const onExport = async () => {
if (!capabilities.export) throw new Error('EXPORT_FORBIDDEN');
await exportAs(editor, [...editor.getCurrentPageShapeIds()], { format: 'svg' });
};
(globalThis as typeof globalThis & { exportCanvas?: () => Promise<void> }).exportCanvas =
onExport;
return () => {
delete (globalThis as typeof globalThis & { exportCanvas?: () => Promise<void> })
.exportCanvas;
};
}}
/>
</div>
);
}
The spike exposes the export function globally only for demonstration. Production UI should inject it through an explicit component/service. Server permission remains the real security boundary. The essential data contract for Custom Binding and Custom Record follows. The client and sync server must register the same validator/migration:
import {
BaseRecord,
BindingUtil,
CustomRecordInfo,
RecordId,
T,
TLBaseBinding,
createCustomRecordId,
createCustomRecordMigrationIds,
createCustomRecordMigrationSequence,
createTLStore,
} from 'tldraw';
import { TaskShapeUtil } from './TaskShapeUtil';
export type DependencyBinding = TLBaseBinding<'task-dependency', { kind: 'blocks' | 'relates' }>;
export class DependencyBindingUtil extends BindingUtil<DependencyBinding> {
static override type = 'task-dependency' as const;
static override props = { kind: T.literalEnum('blocks', 'relates') };
override getDefaultProps(): DependencyBinding['props'] {
return { kind: 'blocks' };
}
}
export interface CanvasNote extends BaseRecord<'canvas-note', RecordId<CanvasNote>> {
taskId: string;
text: string;
createdAt: number;
}
const noteVersions = createCustomRecordMigrationIds('canvas-note', { AddCreatedAt: 1 });
export const canvasNoteInfo: CustomRecordInfo = {
scope: 'document',
validator: T.object({
id: T.string,
typeName: T.literal('canvas-note'),
taskId: T.string,
text: T.string,
createdAt: T.number,
}),
migrations: createCustomRecordMigrationSequence({
sequence: [
{
id: noteVersions.AddCreatedAt,
up: (record) => {
record.createdAt ??= 0;
},
down: (record) => {
delete record.createdAt;
},
},
],
}),
createDefaultProperties: () => ({ taskId: '', text: '', createdAt: 0 }),
};
export const newCanvasNote = (taskId: string): CanvasNote =>
({
id: createCustomRecordId('canvas-note'),
typeName: 'canvas-note',
taskId,
text: '',
createdAt: Date.now(),
}) as CanvasNote;
export const createCanvasStore = () =>
createTLStore({
shapeUtils: [TaskShapeUtil],
bindingUtils: [DependencyBindingUtil],
records: { 'canvas-note': canvasNoteInfo },
});
Back to the school bus: Task is the passenger record; TaskShape is the seat card. Replacing the seat-card Library must not change the passenger’s identity. Dependency Binding is a route clue between seats. Readonly covers the front-door button; it is not the police, so the server still checks tickets. A Custom Record is really registered only when placed in createTLStore({ records }). If you merely export a validator object while allowing <Tldraw> to create its default Store, tests never exercise it. Client and sync server must use the same schema options.
Tests prove round-trip adaptation, permissions independent of UI, and Migration opening an old Snapshot:
import { expect, test } from 'vitest';
import { DomainAdapter } from '../DomainAdapter';
test('Domain → Shape → Domain preserves business identity', () => {
const task = { id: 'task-42', title: 'Review contract', status: 'doing' as const, revision: 7 };
const shape = DomainAdapter.toShape(task);
expect(shape.props.domainId).toBe('task-42');
expect(
DomainAdapter.toDomainPatch({ ...shape, props: { ...shape.props, status: 'done' } }, task),
).toEqual({ ...task, status: 'done' });
});
test('the server rejects an unauthorized write that bypasses readonly', () => {
const authorize = (canEdit: boolean, command: { type: string }) =>
canEdit && ['UpdateTask', 'MoveTask'].includes(command.type);
expect(authorize(false, { type: 'MoveTask' })).toBe(false);
expect(authorize(true, { type: 'DeleteWorkspace' })).toBe(false);
});
Run npm ls tldraw @tldraw/sync. Expect both to show exactly 5.3.2, with no deduplication to another version. Run npm exec vitest run src/sdk/tldraw; expect Domain round-trip, v0 Shape migration, Unknown Record, permission, and visual/semantic export to pass. Run npm exec playwright test tests/tldraw-sdk.spec.ts; expect Custom Tool/Shape/Binding, Readonly, keyboard, and export to pass in the pinned browser matrix.
ADR-017: Build / Buy / Hybrid decision
Status: Accepted for spike. It enters production only after the license quote, performance/accessibility regression, and exit rehearsal pass.
Context and constraints: The product is a Visual Operations Canvas that needs text, connectors, multiplayer, History, Clipboard, migration, and delivery within three months. Business Task/Permission/Audit must be backend-authoritative. It is not 3D. Chapter 15’s visible-object scale suits DOM/React culling; GPU has not been shown necessary.
Options: Build (the current Raw Canvas kernel) provides the most control but has the highest delivery and long-term table-stakes cost. Buy (put all data directly in TLStore) is fastest but creates unacceptable business and exit coupling. Hybrid (tldraw owns the editor experience; we own Domain/permissions/audit) meets the time and control requirements. The decision is Hybrid, pinned to tldraw 5.3.2.
Ownership: SDK Store owns the canvas projection, Page/Shape/Binding/Asset references, and editor History. The business backend owns Task, stable business IDs, permissions, audit, asset authorization, and domain version. Scope Session/Presence separately as in Chapters 13 and 18. Domain writes pass only through the Adapter/Command API.
Export: The legal/long-term export is versioned Domain JSON + Asset manifest. Visual export uses SDK SVG/PNG with golden tests. Preserve CanvasPortableV1 (position, dimensions, text, connections, and Asset ID) as a neutral replacement-Renderer format. Never keep only an SDK Snapshot as the sole backup.
Migration and testing: Test the Domain schema and SDK schema as separate migration chains, with real anonymized golden documents. Every upgrade runs typecheck, migration fixtures, adapter round-trip, custom shape/tool/binding, keyboard/screen reader, visual/export, performance, and sync version-skew tests. Release client and server together. Preserve the previous build and a read-only downgrade.
License cost: Development needs no key. For production, include trial/commercial/hobby eligibility, quote, renewal date, allowed hosts, data-transmission behavior by license type, legal/privacy review, and expiry fallback in annual TCO. A commercial project uses commercial licensing and does not evade fees with hobby licensing. Attach the actual contract to the ADR rather than guessing a price. Recheck when the version or terms change; this chapter’s date is not a permanent promise.
Exit strategy: The domain layer never imports tldraw. Every stable ID can be mapped. Generate Domain JSON/PortableV1 daily. A Compatibility Renderer can open the document read-only and perform basic editing. Quarterly, import golden documents into a replacement Renderer in isolation and record unsupported features. The exit sequence is: freeze new SDK features → export and validate → convert bindings/text/assets → dual-read comparison → low-traffic switch → retain a read-only legacy Viewer → revoke the license.
SDK stops being maintained: Pin an audited version and preserve build assets. Assess security risk and the browser-degradation window. Activate the Compatibility Renderer first, then migrate through PortableV1. A fork is temporary risk mitigation; do not assume the team can maintain the entire SDK forever.
Upgrade breaks a Custom Shape: Never overwrite an old Snapshot. In a shadow environment, have the new version read a copy, then run Shape props migration and visual/semantic diffs. If the adapter, geometry, export, or sync gate fails, roll back lockfile and server. If a release is unavoidable, use dual-read/old-write before migration. Never write an irreversible new format directly for old clients.
Consequences: We gain mature interactions and reduce table-stakes implementation. We accept dependencies on licensing, React/DOM rendering characteristics, version upgrades, and sync operations. Adapter, PortableV1, and quarterly exit rehearsals are hard conditions for accepting this dependency, not decorative documentation.
Break it on purpose
| Injected failure | Symptom | Evidence | Fix | Regression test | Recovery |
|---|---|---|---|---|---|
| Treat TLStore as the business database | Reporting/exit depends on SDK fields | Import graph and backup format | Domain + Adapter | Architecture-boundary test | Rebuild projection from Domain |
| Upgrade from 5.3.2 breaks a Shape | Old cards blank or resize | Golden migration/visual diff | Props migration and pinning | Real Snapshots | Roll back client/server |
| Sync server and client versions differ | Room rejects or corrupts data | Handshake/schema logs | Release together | Version-skew suite | Read-only and require refresh |
| Production license key invalid/expired | SDK unavailable in production | License diagnostics and expiry ledger | Correct key and renewal process | Staging host/expiry test | Compatibility Viewer |
| Hide only the Delete button | API can still delete objects | Server audit | Authorize every Command | UI-bypass request | Reject and preserve revision |
| Custom Binding unregistered on server | Sync/validation fails | Schema mismatch | Same schema on both sides | Connect fixture | Block write and retain locally |
| Export preserves only SDK Snapshot | New Library cannot recover semantics | Exit drill lacks Task | Domain JSON + PortableV1 | Quarterly import | Read backend Domain |
| SDK stops being maintained | Nobody fixes new-browser regressions | Risk register | Freeze, replace, or limited fork | Compatibility matrix | Migrate in stages |
Pass with evidence
| Automated evidence | Manual evidence | Passing condition |
|---|---|---|
| Exact-version assertion, adapter round-trip, migration/golden, permission, export, sync-skew, and performance regression | Keyboard/Screen Reader tasks, production-license staging, and quarterly exit rehearsal | All automated gates pass; a person can open core content in an alternative Viewer through PortableV1 |
| Import graph forbids Domain depending on tldraw | Architecture, legal, procurement, and security jointly review the ADR | Ownership, TCO, allowed hosts, renewal, and exit owners are signed off |
| Decision question | This chapter’s answer and evidence |
|---|---|
| Why choose an SDK? | Whiteboard table stakes fit the delivery window and benchmark scale; not because it is popular |
| Who owns the data? | SDK owns the canvas projection; business backend owns Domain/Permission/Audit |
| How do we export/migrate? | Domain JSON + PortableV1 + visual export; golden tests for both migration chains |
| How do we test it? | Full chain of custom extensions, accessibility, visual, export, performance, and sync skew |
| What does the License cost? | Commercial quote/renewal enters TCO; the tutorial does not guess a number |
| How do we exit? | Adapter, stable IDs, neutral format, Compatibility Renderer, and quarterly rehearsal |
- Dependencies pin exactly
tldraw@5.3.2and the matching sync package. - Describe it accurately as “primarily React/DOM/SVG Shapes, with Canvas 2D overlays as an exception,” never all Canvas 2D.
- Describe tldraw sync accurately as server-authoritative, not Yjs/CRDT.
- Production license key, commercial/hobby conditions, renewal, and legal review appear in the ADR.
- Custom Shape, Tool, Binding, Record, Migration, Export, Readonly, and Permission controls have evidence.
- The business model is not the SDK Store, and the exit rehearsal is not hypothetical.
- Produce a final Build / Buy / Hybrid decision and explain the real cost of rejected options.
Explain it to a five-year-old
Do not say “SDK,” “Adapter,” “License,” or “Migration.” Explain why knowing how to make wheels does not mean smelting steel before every trip. Why must you keep the passenger list when buying a chassis? If the vendor changes its connector or closes, how do you bring the vehicle home?
A good jargon-free answer
Taking children to school requires seat belts, brakes, and doors. A mature chassis has already solved those hard problems, so the trip can start sooner. Build everything only when the route is truly unusual and the team is willing to repair every screw forever. Keep children’s names, permissions, and routes in your own ledger; put only the seat cards needed for display and driving on the dashboard, with an inspectable connector translating between them. Before buying, write down the contract, yearly cost, and expiry, and regularly test the ledger in another simple vehicle. If the vendor changes its connector or stops repairing the bus, every passenger can still leave intact.