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 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.
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
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 }] 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>;
} 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
Pick a path
Whether you're wiring up your first store or migrating a production schema, there's a short guide for it.