tldraw/tldraw
@tldraw/store
Active contributors: Steve Ruiz, David Sheldrick, Mime Čuvalo, Mitja Bezenšek
Purpose
@tldraw/store is the reactive in-memory database that backs every tldraw document. It stores typed records, runs reactive queries, validates with a schema, runs migrations on load, fires side-effects on changes, and exposes diffs that downstream code (sync, history) consumes.
Conceptually: a typed Map that emits change diffs and invalidates dependent computeds.
Directory layout
packages/store/
├── package.json
├── README.md
└── src/
├── index.ts
└── lib/
├── Store.ts # the Store class (40 KB / 1.5k lines)
├── StoreSchema.ts # registers record types and migrations (25 KB)
├── StoreSideEffects.ts # before/after hooks (23 KB)
├── StoreQueries.ts # reactive queries by id and by index (22 KB)
├── BaseRecord.ts # the record interface and id helpers
├── RecordType.ts # RecordType<R, RequiredProperties> factory (12 KB)
├── RecordsDiff.ts # diff and squash helpers
├── AtomMap.ts # reactive Map keyed by id
├── AtomSet.ts # reactive Set
├── ImmutableMap.ts # 28 KB persistent map
├── IncrementalSetConstructor.ts
├── migrate.ts # migration framework (20 KB)
├── executeQuery.ts # query AST evaluator
├── devFreeze.ts # frozen-record dev guard
├── setUtils.ts
└── test/Key abstractions
| Type | File | Purpose |
|---|---|---|
Store<R, Props> |
packages/store/src/lib/Store.ts |
The store. Holds records as AtomMaps, fires side-effects, tracks diffs, supports snapshots. |
BaseRecord |
packages/store/src/lib/BaseRecord.ts |
Shape of every stored record ({ id: RecordId<This>; typeName: string }). |
RecordType |
packages/store/src/lib/RecordType.ts |
Factory for a single record type. Holds validators, scope, default props, id constructor. |
StoreSchema |
packages/store/src/lib/StoreSchema.ts |
Registry of record types + migrations. Used by the store to validate, migrate, and serialize. |
StoreSideEffects |
packages/store/src/lib/StoreSideEffects.ts |
before/after hooks for record create/update/delete. |
StoreQueries |
packages/store/src/lib/StoreQueries.ts |
Reactive queries (find one, find many, by index). |
RecordsDiff |
packages/store/src/lib/RecordsDiff.ts |
The diff type emitted by every store mutation: { added, updated, removed }. |
AtomMap, AtomSet |
packages/store/src/lib/AtomMap.ts, AtomSet.ts |
Reactive collections keyed/valued by id. Each entry is its own Atom. |
migrate |
packages/store/src/lib/migrate.ts |
The migration runner — composes record-level and store-level migrations into ordered sequences. |
How it works
graph LR
Caller -->|put / remove| Store
Store -->|fire| BeforeHook[before side-effects]
BeforeHook --> Store
Store -->|mutate atoms| AtomMap
AtomMap -->|invalidate computeds| Subscribers
Store -->|fire| AfterHook[after side-effects]
Store -->|emit diff| Listeners[history, sync]Mutations are batched inside transact (from @tldraw/state) so a single editor.run(() => { ... }) produces one diff and one re-render.
Store.allRecords() returns all records, but the typical access pattern is via StoreQueries:
store.query.records('shape')— reactive array of all records of a type.store.query.record('shape', () => ({ id: { eq: shapeId } }))— find-one.store.query.ids('shape', () => ({ parentId: { eq: pageId } }))— set of matching ids.
Migrations
A schema change becomes a migration. Migrations are ordered and applied on load so old .tldr files can open in newer SDK versions.
- Record-level migrations are declared on the record type (e.g., a new optional prop with a default value).
- Store-level migrations are declared on the schema (e.g., introducing a new record type or relationship between records).
- The runner is in
migrate.ts. The framework supports up- and down-migrations. - Tests live in
packages/store/src/lib/migrate.test.tsand exhaustively inpackages/tlschema/src/migrations.test.ts(66 KB).
Snapshots
Store.getStoreSnapshot() returns a serializable StoreSnapshot with the schema version. Store.loadSnapshot(snapshot) runs migrations and rehydrates. The dotcom and VS Code apps use this to read/write .tldr files.
Integration points
- Imports.
@tldraw/state(signals, transactions),@tldraw/utils(helpers),@tldraw/validate(validators). - Importers.
@tldraw/tlschema(defines record types),@tldraw/editor(owns the store),@tldraw/sync-core(syncs the store between clients/server).
Entry points for modification
- Add a new record type — define
RecordTypeintlschema, then register it in the schema returned bycreateTLSchema. - Add a side-effect — call
editor.store.sideEffects.registerBeforeChangeHandler(...)(or after). - Add a query — extend
StoreQueriesif it's general; otherwise, build aComputedin your code that reads from existing queries. - Change a migration — never modify an existing migration; add a new one.
Related
- Tlschema for the records that ship with tldraw.
- Store and migrations for the cross-cutting feature view.
- @tldraw/state for the underlying reactive primitives.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.