browser-sqlite
v1.0.0-rc.10
Published
Browser SQLite with concurrent read / serial write isolation, backed by Web Workers and wa-sqlite
Maintainers
Readme
browser-sqlite
A persistent SQLite database that lives in your browser — yes, for real. Powered by wa-sqlite (WebAssembly), built for (read) concurrency.
A client stores its data through a wa-sqlite VFS, which decides where that data lives. Read the VFS page to compare them, then run the benchmarks in your own browser.
Opinionated by design
browser-sqlite makes choices for you, so that one database stays correct while several workers, clients and tabs use it. You will meet each of them as a refusal or a wait if you do not know them, so here they are:
- One writer at a time, across the origin. Every write takes a lock per database shared by every client and every tab; a second writer waits its turn. See Writes are serialized.
- Reads and writes go through different methods.
read(),stream(),chunk()andfirst()accept only a statement that is provably a read; everything else goes throughwrite(). - Transactions belong to the library.
BEGIN,COMMIT,ROLLBACKand savepoints sent as SQL are refused: usetransaction()andtx.savepoint(). A write transaction always opens withBEGIN IMMEDIATE, areadOnlyone with a plainBEGIN. - The build follows the browser. Leave
buildout and each VFS loads the first build it declares that the engine supports.
Install
npm install browser-sqlite
# or
pnpm add browser-sqliteRequires a bundler that supports Web Workers with dynamic imports — or no bundler at all.
Works with no configuration under rsbuild 1+, rspack 1+, Parcel 2+, Vite 8+, webpack 5.101+ — and with no bundler at all.
Works under Vite 6.1 to 7 with the following config, which only the dev server needs:
// vite.config.ts
export default defineConfig({
optimizeDeps: { exclude: ['browser-sqlite'] },
});Another bundler will likely work — the worker and its .wasm are reached through plain, statically analysable URLs — but may need configuration of its own.
The .wasm are read from beside worker.js. If a build separates them, or you move them by hand, point at them with wasmUrl.
Browser support
| Chrome | Firefox | Safari | |---|---|---| | 92+ | 90+ | 15.4+ |
Cross-origin isolation is worth adding where you control your headers: it is what lets an aborted call stop a running statement when using a VFS with sync build. See Aborting a call.
Usage
Read the detailed API documentation for the full description.
import { createSQLiteClient, sql } from 'browser-sqlite';
const db = createSQLiteClient('myapp.sqlite', { vfs: 'OPFSAdaptiveVFS' });
await db.write('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
await db.write(sql`INSERT INTO users (name) VALUES (${'Alice'})`);
const users = await db.read<{ id: number; name: string }>('SELECT id, name FROM users');
for await (const row of db.stream<{ id: number; name: string }>('SELECT * FROM users')) {
process(row);
}
await db.close();client.id · client.name · client.file · client.files · client.vfs · client.build · client.poolSize · client.ready · client.debug
createSQLiteClient() · client.read() · client.write() · client.stream() · client.chunk() · client.first() · client.transaction() · client.bulkWrite() · client.output() · client.inspect() · client.close() · deleteDatabase() · inspectDatabase() · sql`…`
Storage
The VFS decides where your database is written. See every available VFS on the dedicated page, with their pros, their cons, their limitations and their browser compatibility.
OPFSWriteAheadVFS and OPFSAdaptiveVFS are the recommended options.
OPFSWriteAheadVFS · OPFSAdaptiveVFS · OPFSCoopSyncVFS · AccessHandlePoolVFS · IDBBatchAtomicVFS · IDBMirrorVFS · OPFSAnyContextVFS · MemoryVFS · MemoryAsyncVFS
Guarantees
Reads run concurrently
Every read is dispatched to whichever worker in the pool is free, so several run at once. Writes take a dedicated writer worker instead, one at a time.
Read-your-own-writes
Once a write has resolved, any read issued afterwards observes it — from any client, in this tab or another, whatever the pool size. The one exception is IDBMirrorVFS, which does not hold it across tabs.
Writes are serialized
write(), a write transaction() and each batch of a bulkWrite() take one lock per database, across every client and tab: a second writer waits its turn, unbounded unless you pass a signal or a timeout. A transaction holds the lock for its whole callback, so keep it short. A bulkWrite() commits per batch — use tx.bulkWrite() for all or nothing.
Known Limitations
Some VFS have limitations of their own — see the detailed VFS page. What follows holds on all of them.
Aborting a call
An abort does not always stop the work, and your hosting decides. On the sync build, a signal or a timeout rejects your promise straight away, but the statement runs to its end on its worker, which stays unavailable until it does.
Serving the page cross-origin isolated gets you out, and so does any build other than sync; what each one costs is under Interrupting a call.
Deleting a database
A database that any client still holds cannot be deleted, in this tab or another, on every VFS. More on the dedicated deleteDatabase API entry.
Development
pnpm install
pnpm build # rslib → dist/
pnpm test # unit (Node) + browser (Playwright: Chromium, Firefox)
pnpm check # biome, with --writeThese suites run on demand rather than on every change:
pnpm test:conformance # every declared (vfs, build) pair through the same invariants
pnpm test:consumer # packs the tarball and runs it under each supported bundler, and with noneThe benchmark page
scripts/bench/html/index.html is the page published above. It is one self-contained file served beside a verbatim copy of dist/, so it exercises the library exactly as a consumer would with no bundler at all.
pnpm bench:dev # build, serve on http://127.0.0.1:8099, rebuild on change
pnpm bench:serve # same without the watch
pnpm bench:build # assemble _site/ onlyhttp://127.0.0.1 is a secure context, so OPFS works with no certificate — no TLS setup is needed to develop against it. A phone on the LAN is a different matter: it is not a secure context, so browsers withhold OPFS and the Web Locks API there, and only MemoryVFS and MemoryAsyncVFS run. A tunnel (or the published page) is the way to test a real device.
node scripts/bench/check.ts [chromium|firefox] [--all] drives the page under Playwright and asserts that it still works — it is run by hand and deliberately not wired into CI. It checks the page, never that a VFS passes: a red cell can be a correct report about the engine you are on.
