Skip to content

case_study

granthdb

SQLite in the browser, behind a Dexie-compatible API

Solo — architecture, engineering, docs, and release

  • TypeScript
  • SQLite/WASM
  • OPFS
  • Web Workers
  • Web Locks
granthdb: SQLite in the browser, behind a Dexie-compatible API

problem

The problem

IndexedDB is the only substantial storage the browser offers, and it has no query planner. One index per query, and everything else is a cursor walked on the main thread. Filtering on one field and sorting by another — the shape almost every list view needs — is not expressible, so in practice you fetch the rows and sort them in JavaScript.

SQLite compiled to WebAssembly solves the query half, and immediately creates a harder problem. OPFS sync access handles are exclusive: two tabs writing one database file is a corrupted database, not a race you can retry. The obvious VFS also requires cross-origin isolation, which means COOP/COEP headers, which breaks every third-party embed on the page.

So the interesting work was never the SQL. It was keeping a Dexie-shaped API honest on top of a completely different storage engine, and making multi-tab access safe without asking anyone to change their response headers.

approach

What I engineered

  • Built the storage on the opfs-sahpool VFS, which needs no COOP/COEP — the same constraint that pushed Notion to the same choice — with automatic fallback to IndexedDB and then to memory, so Safari private browsing gets a working database rather than an exception.
  • Elected a single writer across tabs over Web Locks, with change announcements on a BroadcastChannel. The RPC acknowledges before it runs, which lets the client separate 'the leader vanished and the commit state is unknown' from 'no leader was reached, so retrying is safe' — two failures that look identical from the outside and must be handled differently.
  • Kept documents as JSON with declared indexes as generated columns over json_extract, so SQLite's planner does real index work on a document store, including compound and multiEntry indexes backed by trigger-maintained shadow tables.
  • Verified compatibility by differential testing rather than by name-matching: the same script runs against the real dexie package and against granthdb, and the answers are diffed. A name-level audit reported full coverage while delete('2') was destroying a row — that suite found 27 disagreements a green audit could not see.
  • Wrote a value codec against the structured clone algorithm so a Date, a BigInt, a typed array, a Map or a RegExp comes back as itself rather than as JSON's nearest approximation — and made Blob and File fail loudly instead of storing an empty object.
  • Shipped the surrounding surface as first-class work: a codemod for Dexie migration, an IndexedDB importer with schema inference, React and Vue bindings, a field-level AES-GCM encryption addon whose test greps the raw file for plaintext, and an MCP server so a coding assistant can run granthdb code instead of guessing at the API.

result

The result

A published library at 92% Dexie API parity, measured by an audit that runs against the real dexie package in CI and fails the build on any un-waived gap. The eight members it does not implement are exported as data with a reason each, so the gaps are inspectable rather than folklore.

The architecture converged independently on the one Notion published for the same problem — same VFS, no cross-origin isolation, one elected writer — differing only in electing over Web Locks directly rather than through a SharedWorker.

The parts I am most confident about are the ones where a guard was written and then deliberately broken to watch it fail: the multi-tab failover, the trigger-scan regression, the cross-fade that had been dipping to background, and the codec that was silently destroying binary. Each of those had passed a suite that could not have caught it.

92%
Dexie API parity, audited in CI
14
packages, MIT, no server
0
COOP/COEP headers required

Product facts for granthdb, my own MIT-licensed open-source library (2026). Parity is the audit's own figure — 92 of 100 members across Dexie, Table, Collection and WhereClause, measured against dexie 4.4.5 — and the build fails on any gap not explicitly waived. No adoption or download figures are claimed.See the parity audit

work_with_me

Have a project like this?

Tell me the problem on a 20-minute call and I'll tell you how I'd approach it, the same way granthdb started.

Discuss your project