Skip to content

LLM Cheat Sheet

Use this file as short LLM context for HyperDB. Full docs: https://hyperdb.will-be-done.app/

Good deep links:

HyperDB is a TypeScript database layer for local-first apps. It gives you typed schemas, indexed reads, transactional writes, selector reactivity, React hooks, and pluggable storage drivers. The same schema, selectors, and actions can run in the browser and on the server; only the storage driver changes.

Use it when an app needs structured reactive data instead of plain component state, Redux arrays, or hand-written indexes. It is especially useful for offline/local-first apps, sorted collections, reusable client/server data logic, and views that should re-run only when the exact index ranges they read change.

  • @will-be-done/hyperdb: core APIs such as schemas, validators, selectors, actions, runtimes, command executors, HybridDB, SubscribableDB, tracing setup, and core types.
  • @will-be-done/hyperdb/react: DBProvider, useDB, useOptionalDB, useSyncSelector, useAsyncSelector, useDispatch, useAsyncDispatch, useSelect, and useAsyncSelect.
  • @will-be-done/hyperdb/tracing: tracing store, tracer configuration, and trace metadata helpers.
  • @will-be-done/hyperdb/drivers/inmemory: BptreeInmemDriver.
  • @will-be-done/hyperdb/drivers/sqlite: SqlDriver, AsyncSqlDriver, and SQLite adapter types.
  • @will-be-done/hyperdb/drivers/idb: openIndexedDBDriver, IdbDriver, and IndexedDB driver options.
  • @will-be-done/hyperdb-devtool/react: separate package with HyperDBDevtools and HyperDBDevtoolsPanel.
import {
DB,
HybridDB,
SubscribableDB,
asyncDispatch,
createAction,
createSelector,
defineTable,
deleteRows,
execAsync,
execMaybeAsync,
execSync,
getCurrentTraits,
insert,
or,
preloadSelector,
select,
selectAsync,
selectFrom,
syncDispatch,
upsert,
v,
type ExtractSchema,
type HyperDB,
} from "@will-be-done/hyperdb";

Every table needs a string id. HyperDB creates a built-in unique hash index named byId. Add B-tree indexes for sorted/range reads, hash indexes for non-unique exact single-column lookups, and uniqhash indexes for exact values that must be unique.

import { defineTable, v, type ExtractSchema } from "@will-be-done/hyperdb";
export const tasksTable = defineTable("tasks", {
id: v.string(),
projectId: v.string(),
title: v.string(),
slug: v.string(),
state: v.union(v.literal("todo"), v.literal("done")),
orderToken: v.string(),
completedAt: v.optional(v.number()),
})
.index("byProjectOrder", ["projectId", "orderToken"])
.index("bySlug", ["slug"], { type: "uniqhash" })
.index("byIds", ["id"]);
export type Task = ExtractSchema<typeof tasksTable>;

Useful validators: v.string(), v.number(), v.bigint(), v.boolean(), v.null(), v.arrayBuffer(), v.array(...), v.object(...), v.record(...), v.union(...), v.literal(...), v.optional(...), v.partial(...), v.required(...), v.lazy(...), v.pass<T>(), and v.any().

Stored rows cannot contain undefined except where optional object fields are omitted. Arrays and records cannot contain undefined.

Create one shared selector builder and reuse it.

import { createSelector, selectFrom, v } from "@will-be-done/hyperdb";
import { tasksTable } from "./schema";
export const selector = createSelector({
validateArgs: process.env.NODE_ENV === "development",
});
export const projectTasks = selector({
name: "projectTasks",
args: { projectId: v.string() },
handler: function* ({ projectId }) {
return yield* selectFrom(tasksTable, "byProjectOrder")
.where((q) => q.eq("projectId", projectId))
.order("asc");
},
});

selectFrom(table, index) supports .where(...), .order("asc" | "desc"), .limit(n), .first(), and .firstOr(fallback). Filter operators are eq, lt, lte, gt, and gte.

OR queries can return an array or use or(...):

yield *
selectFrom(tasksTable, "byProjectOrder").where((q) =>
or(q.eq("projectId", "p1"), q.eq("projectId", "p2")),
);

Run selectors outside React with select, selectAsync, or preloadSelector. Use sync helpers with sync drivers and async helpers with async drivers:

const rows = select(db, projectTasks({ projectId: "p1" }));
const asyncRows = await selectAsync(db, projectTasks({ projectId: "p1" }));

Create one shared action builder and reuse it. Actions are generator functions that run inside a transaction. Use insert, upsert, and deleteRows.

import {
asyncDispatch,
createAction,
insert,
selectFrom,
upsert,
v,
} from "@will-be-done/hyperdb";
import { tasksTable } from "./schema";
export const action = createAction({
validateArgs: process.env.NODE_ENV === "development",
});
export const createTask = action({
name: "createTask",
args: { id: v.string(), projectId: v.string(), title: v.string() },
handler: function* ({ id, projectId, title }) {
yield* insert(tasksTable, [
{ id, projectId, title, slug: id, state: "todo", orderToken: id },
]);
},
});
export const completeTask = action({
name: "completeTask",
args: { id: v.string() },
handler: function* ({ id }) {
const task = yield* selectFrom(tasksTable, "byId")
.where((q) => q.eq("id", id))
.first();
if (!task) return;
yield* upsert(tasksTable, [{ ...task, state: "done" }]);
},
});
await asyncDispatch(
db,
createTask({ id: "t1", projectId: "p1", title: "Ship" }),
);

Use await asyncDispatch(db, action(...)) for HybridDB and other asynchronous drivers. Use syncDispatch(db, action(...)) only for synchronous drivers. insert fails on duplicate ids, upsert replaces whole rows by id, and deleteRows deletes by id.

For durable frontend data, prefer HybridDB: persistent primary storage plus an in-memory cache. The primary can be IndexedDB or async SQLite; the cache is normally BptreeInmemDriver.

import { DB, HybridDB, SubscribableDB, execAsync } from "@will-be-done/hyperdb";
import { openIndexedDBDriver } from "@will-be-done/hyperdb/drivers/idb";
import { BptreeInmemDriver } from "@will-be-done/hyperdb/drivers/inmemory";
import { tasksTable } from "./schema";
export async function createAppDB() {
const primary = new DB(await openIndexedDBDriver("my-app"), {
runtimeRowsValidation: process.env.NODE_ENV === "development",
freezeArgs: process.env.NODE_ENV === "development",
freezeRows: process.env.NODE_ENV === "development",
});
const cache = new DB(new BptreeInmemDriver());
const db = new SubscribableDB(new HybridDB(primary, cache));
await execAsync(db.loadTables([tasksTable]));
await execAsync(
db.preloadTables([{ table: tasksTable, scanIndex: "byIds" }]),
);
return db;
}

Use a B-tree full-scan index such as byIds for preloadTables; the built-in byId index is a unique hash index for exact id lookups. SubscribableDB adds revisions, subscriptions, selector invalidation, and lifecycle hooks. Pure in-memory apps can skip HybridDB and use new SubscribableDB(new DB(new BptreeInmemDriver())).

HybridDB readwrite transactions commit to the in-memory cache first and flush their final row changes to the persistent primary afterward. This keeps asyncDispatch responsive for UI writes. Cached scan intervals keep reading from memory while persistence is pending; uncached scans wait for the pending flush only when the pending old or new row values can affect the requested interval. Exact uniqhash lookups are marked cached when rows are loaded from persistence or when the cache transaction commits, so byId and other unique equality reads can return from memory while persistence is still pending. Non-unique hash buckets are only marked cached when the hash scan itself proves the whole bucket. Write transaction scans reuse coverage already known by the committed cache.

Use new HybridDB(primary, cache, { debug: true }) to log why an uncached scan fell through to persistence, waited for pending persistence, or retried a failed background flush. Use a callback for structured HybridDBDebugEvent objects.

If a background flush keeps failing past its bounded retries, HybridDB enters a permanent crashed state: hybrid.isCrashed becomes true and every subsequent read, write, or transaction (including cache-only reads) throws HybridDBCrashedError with the persistence error as its cause. Recover by creating a new HybridDB and reloading tables.

import {
DBProvider,
useAsyncDispatch,
useAsyncSelector,
} from "@will-be-done/hyperdb/react";
import type { SubscribableDB } from "@will-be-done/hyperdb";
import { HyperDBDevtools } from "@will-be-done/hyperdb-devtool/react";
import { createTask, projectTasks } from "./tasks";
function Tasks({ projectId }: { projectId: string }) {
const { data: tasks = [], isFetching } = useAsyncSelector({
selector: projectTasks,
args: { projectId },
defaultValue: [],
});
const dispatch = useAsyncDispatch();
return (
<button
disabled={isFetching}
onClick={() =>
void dispatch(
createTask({
id: crypto.randomUUID(),
projectId,
title: "New task",
}),
)
}
>
Add {tasks.length + 1}
</button>
);
}
export function App({ db }: { db: SubscribableDB }) {
return (
<DBProvider value={db}>
<Tasks projectId="p1" />
<HyperDBDevtools db={db} initialIsOpen={false} />
</DBProvider>
);
}

Use useAsyncSelector and useAsyncDispatch with HybridDB. useAsyncSelector returns a React Query-style result with data, status, error, fetching flags, and refetch(). Use useSyncSelector / useDispatch only for purely synchronous drivers.

  • Prefer selectors for reads and actions for writes.
  • Do not write inside selectors.
  • Define indexes for every query shape; query filters must fit the selected index.
  • Use byId for id lookups.
  • Prefer HybridDB for durable browser state: persistent primary, in-memory cache, async selectors, and async dispatch.
  • Use B-tree indexes for ordering, ranges, composite keys, and full-table preloading.
  • Use hash indexes only for exact non-unique single-column equality; use uniqhash when the value must be unique.
  • For partial updates, read the current row and upsert the complete next row.
  • Keep schema, selectors, and actions in shared modules/packages so client and server can import the same data layer.