Getting started
Ledger is a tiny reactive store for TypeScript. In about five minutes you'll install it, define a typed collection, write a live query, and render it in React. No server, no build plugin, no config.
Install
Ledger ships as an ESM package with bundled type definitions. Add it with your package manager of choice:
# npm
npm install ledger
# pnpm
pnpm add ledger
# bun
bun add ledger Ledger requires TypeScript 5.0+ for full inference, but it runs fine in plain JavaScript — you just lose the compile-time checks.
Create a store
A store is a map of named collections. Each collection is typed by the record it holds, and you tell it which field is the primary key.
import { createLedger, collection } from 'ledger';
type Note = {
id: string;
title: string;
body: string;
pinned: boolean;
createdAt: number;
};
export const db = createLedger({
notes: collection<Note>({ key: 'id' }),
});
That's the whole schema. db.notes is now a fully-typed collection: TypeScript
knows its records are Note objects keyed by id.
Insert and read
Inserts are synchronous and type-checked. Reads come straight from the in-memory index:
db.notes.insert({
id: crypto.randomUUID(),
title: 'Welcome to Ledger',
body: 'Edit me, delete me, query me.',
pinned: true,
createdAt: Date.now(),
});
// Read everything, or read one record by key:
const all = db.notes.all();
const one = db.notes.get('some-id'); Inserting a record whose key already exists throws. Use upsert() if you want insert-or-replace semantics instead.
Go reactive
The point of Ledger is live data. Turn any query into a subscription with
.live(), and your callback fires once immediately and again every time
the result changes — and only when it actually changes.
// .live() returns a Query you can subscribe to.
const pinned = db.notes
.where((n) => n.pinned)
.sort('createdAt', 'desc')
.live();
const stop = pinned.subscribe((notes) => {
console.log('pinned notes changed:', notes.length);
});
// Later, when you're done:
stop(); Wire up React
The ledger/react binding wraps all of that in a single hook. Pass a function
that returns a live query; the component re-renders with fresh data and unsubscribes on unmount.
import { useQuery } from 'ledger/react';
import { db } from './db';
export function PinnedNotes() {
const notes = useQuery(() =>
db.notes.where((n) => n.pinned).sort('createdAt', 'desc').live()
);
return (
<ul>
{notes.map((n) => (
<li key={n.id}>{n.title}</li>
))}
</ul>
);
}
Vue and Svelte bindings (ledger/vue, ledger/svelte) follow the same shape.
Next steps
You have a working reactive store. From here:
- Reactive queries — filters, sorting, and derived queries in depth.
- Persistence — keep your data across reloads with the IndexedDB adapter.
- Schema migrations — evolve your data shape safely as the app grows.
- API reference — every method and option, with examples.