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

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