Skip to content

About

WASM Sqlite wrapper with worker pool

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

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.
  • One statement per call. A string that holds several statements is refused with MULTIPLE_STATEMENTS before anything runs: send each in its own call, or in a transaction(). run(), all(), stream() and chunk() take any single statement, a write included; only first() is for reads and refuses a statement that writes.
  • Transactions belong to the library. BEGIN, COMMIT, ROLLBACK and savepoints sent as SQL are refused: use transaction() and tx.savepoint(). A write transaction always opens with BEGIN IMMEDIATE, a readOnly one with a plain BEGIN.
  • The build follows the browser. Leave build out and each VFS loads the first build it declares that the engine supports.

Install

npm install browser-sqlite
# or
pnpm add browser-sqlite

Requires a bundler that supports Web Workers with dynamic imports — or no bundler at all.

Bundler Configuration

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.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
await db.run(sql`INSERT INTO users (name) VALUES (${'Alice'})`);

const users = await db.all<{ 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.run() · client.all() · 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

A statement that writes, 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 (Vitest on WebdriverIO: Chrome, Firefox) — Linux, Windows, macOS
pnpm check          # biome, with --write

The browsers are the versions scripts/browsers.ts pins, downloaded on first use. On a fresh machine, run node scripts/browsers.ts install once before the first pnpm test: its parallel jobs would otherwise download the same browsers at once.

CI runs the suites on Linux for every pull request and push of main. The Windows and macOS runs are started by hand, from the Actions tab or with gh workflow run tests-windows.yaml --ref <branch> (tests-macos-26.yaml and tests-macos-27.yaml likewise, and tests-ubuntu.yaml for Linux outside CI); a workflow can be started this way once it exists on main.

These 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 none

The 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/ only

http://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 [chrome|firefox] [--all] drives the page under WebdriverIO 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.

About

WASM Sqlite wrapper with worker pool

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages