You're viewing the v2.3 docs.
Ledger docs
v2.4 · local-first state for TypeScript

Reactive data for web apps, in 4 kilobytes.

Local-first state for the web. Define typed collections, write reactive queries, and persist to IndexedDB — in 4 kB, with zero dependencies.

$ npm i ledger
A code editor on a dark theme showing TypeScript source on a developer's screen
Why Ledger

A data layer that gets out of your way

Everything you need to model, query, and persist app state — without dragging in a database server or a megabyte of client code.

Type-safe by default

Collections are generic. Inserts, queries, and results are fully inferred — no codegen, no decorators.

Reactive queries

Call .live() on any query and subscribe. Ledger recomputes only what changed and emits the new result.

Local-first

Pluggable IndexedDB and localStorage adapters keep data on the device and survive reloads and offline use.

4 kB, zero deps

The whole core is 4 kB min+gzip with no runtime dependencies. Tree-shakeable down to what you import.

Framework bindings

First-party hooks for React, Vue, and Svelte. The core is framework-agnostic and works in plain JS too.

Safe migrations

Version your schema and ship migration steps. Ledger upgrades stored data on load, in order, exactly once.

Quick look

Define a collection, write a live query

Ledger queries are plain functions over typed records. Add .live() and you get a subscription that stays in sync as data changes — no manual invalidation.

  • Full type inference from your record types
  • Chainable where / sort / limit
  • Fine-grained updates — subscribers fire only when their result changes
todos.ts typescript
import { createLedger, collection } from 'ledger';

type Todo = { id: string; text: string; done: boolean };

const db = createLedger({
  todos: collection<Todo>(),
});

// A live query re-runs whenever matching data changes.
const active = db.todos.where((t) => !t.done).sort('text').live();
active.subscribe((todos) => render(todos));

db.todos.insert({ id: 't1', text: 'Ship the docs', done: false });
// → subscriber fires with [{ id: 't1', text: 'Ship the docs', done: false }]
TodoList.tsx tsx
import { useQuery } from 'ledger/react';

function TodoList() {
  const todos = useQuery(() => db.todos.where((t) => !t.done).live());
  return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
Framework bindings

One hook, zero boilerplate

The React binding turns any live query into reactive component state. The same query works untouched on the server, in a worker, or in plain JavaScript.

  • Hooks for React, Vue, and Svelte
  • Automatic subscribe / unsubscribe on mount
  • SSR-safe — queries run anywhere
4 kBmin + gzip
0dependencies
100%typed API
MITlicensed
Start here

Pick a path

Whether you're wiring up your first store or migrating a production schema, there's a short guide for it.

Esc
↑↓ navigate ↵ open Esc close