Open-Source Wikis

/

tldraw

/

Packages

/

@tldraw/store

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.ts and exhaustively in packages/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 RecordType in tlschema, then register it in the schema returned by createTLSchema.
  • Add a side-effect — call editor.store.sideEffects.registerBeforeChangeHandler(...) (or after).
  • Add a query — extend StoreQueries if it's general; otherwise, build a Computed in your code that reads from existing queries.
  • Change a migration — never modify an existing migration; add a new one.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

@tldraw/store – tldraw wiki | Factory