Spec-compliant IndexedDB for server runtimes, with pluggable in-memory and SQLite backends.
A pure-TypeScript implementation of the W3C IndexedDB API
that runs on Node.js and Bun. Unlike test doubles such as fake-indexeddb, the
SQLite backend persists to disk, so the same IndexedDB code you write for the
browser works — and keeps its data — on the server.
- Spec-compliant: validated against the Web Platform Tests IndexedDB suite.
- Pluggable backends:
MemoryBackend(pure JS, no dependencies) andSQLiteBackend(persistent). - Runs on Node and Bun: the SQLite backend auto-detects
bun:sqliteorbetter-sqlite3. - Full API surface: transactions, cursors, indexes (including
multiEntryandunique), key ranges, and order-preserving key encoding. - Correct concurrency: readwrite transactions with overlapping scopes are serialized per the spec; readonly transactions overlap.
- No web platform dependencies: does not require the File System Access API or any browser globals.
npm install @b9g/indexeddbThe MemoryBackend has no dependencies. For the SQLiteBackend:
-
On Bun, nothing extra is needed — it uses the built-in
bun:sqlite. -
On Node.js, install the optional peer dependency:
npm install better-sqlite3
import {IDBFactory} from "@b9g/indexeddb";
import {SQLiteBackend} from "@b9g/indexeddb/sqlite";
// One factory owns a directory of SQLite database files.
const indexedDB = new IDBFactory(new SQLiteBackend("./data"));
const db = await new Promise((resolve, reject) => {
const request = indexedDB.open("my-app", 1);
request.onupgradeneeded = () => {
const store = request.result.createObjectStore("todos", {
keyPath: "id",
autoIncrement: true,
});
store.createIndex("by-done", "done", {unique: false});
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const tx = db.transaction("todos", "readwrite");
tx.objectStore("todos").add({title: "Ship it", done: false});The same code runs against the in-memory backend — swap the backend, keep the API:
import {IDBFactory} from "@b9g/indexeddb";
import {MemoryBackend} from "@b9g/indexeddb/memory";
const indexedDB = new IDBFactory(new MemoryBackend());| Backend | Persistence | Dependencies | Best for |
|---|---|---|---|
MemoryBackend |
None (in-memory) | None | Tests, SSR, ephemeral state |
SQLiteBackend |
Disk (per file) | bun:sqlite or better-sqlite3 |
Persistent single-owner storage |
The SQLiteBackend is designed for single-owner databases. It keeps one shared,
refcounted SQLite handle per database name, and the transaction scheduler that
enforces spec ordering lives in-process. This means:
- It scales vertically — one process owns a database and serves it with WAL and sensible pragmas already configured.
- It does not coordinate across processes. Two processes writing the same file
fall back to SQLite's
busy_timeoutand can contend.
To scale out, give each database a single owner (for example, one worker per database) and shard ownership — rather than sharing a file across processes.
Values are serialized with the structured clone algorithm (node:v8), so
Map, Set, Date, RegExp, ArrayBuffer, typed arrays, and cyclic
references all round-trip — through both backends.
Blob and File are supported on Bun (bytes, type, name, and
lastModified are preserved). On runtimes whose serializer cannot preserve them
— notably Node.js, whose node:v8 drops a Blob to an empty object — storing a
Blob/File throws a DataCloneError instead of silently corrupting the value.
Store the bytes as an ArrayBuffer there, or run on Bun.
- The SQLite driver is synchronous. Each operation runs on the calling thread; very large values block during serialization and write.
The suite is validated against the official
Web Platform Tests IndexedDB suite,
run against both backends. The real WPT .any.js files are fetched at a pinned
revision (sparse, blobless — a few MB, not the full WPT tree) and executed through a
small testharness shim.
bun run wpt:setup # fetch the pinned WPT IndexedDB tests into ./wpt (once)
bun test # runs unit tests + the WPT conformance suitebun run test runs wpt:setup automatically first. The test layout:
| Files | What it runs |
|---|---|
test/*.test.ts |
Unit tests for keys, cursors, transactions, backends |
test/spec-*.test.ts |
A hand-written spec suite, per backend |
test/wpt-*.test.ts |
The real WPT .any.js files, per backend |
MIT © Brian Kim