sqlite-promiser
v1.0.2
Published
Async SQLite WASM with OPFS persistence (with safe fallbacks) for frontend apps.
Maintainers
Readme
sqlite-promiser
Async SQLite in the browser via @sqlite.org/sqlite-wasm loaded with sqlite3InitModule() and OO API #1 inside a small dedicated worker (see SQLite WASM loading). Your app still uses await on the main thread; the deprecated Worker1 promiser is not used.
- OPFS — when
crossOriginIsolatedis true andnavigator.storage.getDirectoryexists (typical with COOP/COEP headers). - Fallback — in-memory SQLite (session-only) when isolation or OPFS is unavailable.
Install
pnpm add sqlite-promiser @sqlite.org/sqlite-wasm@sqlite.org/sqlite-wasm is a peer dependency of sqlite-promiser, so your app controls which SQLite WASM version is used (install any compatible version you prefer).
Published dist/ always contains these same-origin siblings (do not omit any when copying or serving the worker):
sqlite-oo1-worker.js— bundledsqlite3InitModule()+ OO1 RPCsqlite3.wasm— copied from your installed@sqlite.org/sqlite-wasmsqlite3-opfs-async-proxy.js— copied from upstream and lightly patched for nested-worker contexts; required when SQLite opens the OPFS async VFS (see below)
Subpath exports for asset pipelines: sqlite-promiser/worker, sqlite-promiser/sqlite3.wasm, sqlite-promiser/opfs-async-proxy. Optional sqlite-promiser/vite provides a small dev plugin. See gotchas and loading.
Quick start
import { createDatabase, describeEnvironment, headersPresets } from 'sqlite-promiser';
console.log(describeEnvironment());
const db = await createDatabase({ name: 'app', preferOpfs: true });
await db.exec(`CREATE TABLE IF NOT EXISTS todo (id INTEGER PRIMARY KEY, title TEXT)`);
await db.run(`INSERT INTO todo (title) VALUES (?)`, ['Buy milk']);
const rows = await db.all(`SELECT id, title FROM todo`);
console.log(rows);
await db.close();Diagnostics
const { mode, persistent, vfs, crossOriginIsolated, opfsAvailable } = db.getDiagnostics();
// mode: 'opfs' | 'memory'OPFS and nested workers
For durable OPFS storage, SQLite’s WASM build uses two workers from your page’s perspective:
- The OO1 worker (
sqlite-oo1-worker.js) — wheresqlite3InitModule()runs and your app’s RPC lands. - An OPFS async proxy (
sqlite3-opfs-async-proxy.js) — a nestedWorkerspawned by upstream code vianew URL("sqlite3-opfs-async-proxy.js", import.meta.url)relative to the OO1 worker script.
If the proxy file is missing (404 HTML, wrong MIME type, or wrong directory), DevTools may show blocked or opaque failures even when COOP/COEP is correct. Keep the proxy next to the OO1 worker URL. Some embedded or strict Chromium builds can still fail the nested worker; the published proxy replaces a fragile globalThis.window === globalThis check with typeof document !== "undefined" so main thread is not confused with a nested classic worker where DedicatedWorkerGlobalScope may be unavailable.
You cannot remove SQLite’s internal OPFS helper worker without changing upstream; apps that previously used a single Vite worker with OpfsDb directly still had this nesting inside that worker. The stable integration path here is correct URLs + headers, not a separate worker graph on the main thread.
Cross-origin isolation (OPFS)
OPFS-backed persistence needs a cross-origin isolated context (SharedArrayBuffer, Atomics, etc.). require-corp is the common default:
headersPresets().requireCorp;
// => {
// 'Cross-Origin-Opener-Policy': 'same-origin',
// 'Cross-Origin-Embedder-Policy': 'require-corp',
// }credentialless is often easier when you rely on third-party iframes or assets that do not set CORP/CORS the way require-corp demands; behavior can differ by browser, so validate in your target environments.
headersPresets().credentialless;Use describeEnvironment() in UI to explain why storage is in-memory.
Vite
examples/example-vite uses sqlite-promiser/vite so COOP/COEP and optional dev origin stay in one place. Its dev / preview scripts set VITE_COOP=1 by default so the page is cross-origin isolated and OPFS can run; use VITE_COOP=0 pnpm dev locally if you want to exercise the in-memory fallback.
VITE_COOP=1— appliesrequireCorpheaders toserverandpreview.VITE_COOP_POLICY=credentialless— use withVITE_COOP=1to applycredentiallessinstead.VITE_DEV_ORIGIN— e.g.https://localhost.example.com:5173when the browser loads the app from a custom HTTPS hostname; setsserver.origin/preview.originso absolute worker or asset URLs match the page origin.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { sqlitePromiserDevPlugin } from 'sqlite-promiser/vite';
export default defineConfig({
plugins: [react(), sqlitePromiserDevPlugin()],
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm']
}
});Manual alternative (avoids pulling the plugin): use sqlite-promiser/headers in config so Node does not load the WASM bundle during config evaluation:
import { headersPresets } from 'sqlite-promiser/headers';
const coop = process.env.VITE_COOP === '1';
export default defineConfig({
server: {
headers: coop ? headersPresets().requireCorp : {}
},
preview: {
headers: coop ? headersPresets().requireCorp : {}
}
});If a custom dev middleware serves *.js workers as HTML or wrong Content-Type, the nested OPFS worker will fail; use application/javascript (or text/javascript) for worker scripts.
Serve the same COOP/COEP headers in production (CDN / static host).
Next.js (App Router)
Use only in client components. Never call createDatabase on the server.
'use client';
import { useEffect } from 'react';
import { createDatabase } from 'sqlite-promiser';
export function DbLoader() {
useEffect(() => {
void (async () => {
const db = await createDatabase({ name: 'app' });
// ...
await db.close();
})();
}, []);
return null;
}Headers for isolation (adjust source / matcher):
// next.config.ts
import type { NextConfig } from 'next';
import { headersPresets } from 'sqlite-promiser/headers';
const nextConfig: NextConfig = {
async headers() {
const h = headersPresets().requireCorp;
return [
{
source: '/:path*',
headers: Object.entries(h).map(([key, value]) => ({ key, value }))
}
];
}
};
export default nextConfig;Copy static assets from sqlite-promiser subpaths (after a library build or from node_modules/sqlite-promiser/dist/) so sqlite-oo1-worker.js, sqlite3.wasm, and sqlite3-opfs-async-proxy.js live in the same public directory. Optionally pass ?sqlite3.wasm= on the worker script URL if WASM is hosted elsewhere.
Plain JS / other bundlers
- Install
sqlite-promiserand@sqlite.org/sqlite-wasm. - Configure COOP/COEP on the HTML origin when you need OPFS.
- Ensure
sqlite-oo1-worker.js,sqlite3.wasm, andsqlite3-opfs-async-proxy.jsare served same-origin with correct MIME types (or pass?sqlite3.wasm=on the worker URL). The OO1 worker resolves the OPFS proxy relative to its own script URL.
React
import { useDatabase } from 'sqlite-promiser/react';
export function Demo() {
const { db, loading, error } = useDatabase({ name: 'app' });
if (loading || error || !db) return <div>{error ? String(error) : '…'}</div>;
return <pre>{JSON.stringify(db.getDiagnostics(), null, 2)}</pre>;
}API
| Export | Purpose |
|--------|---------|
| createDatabase(options) | Open DB (worker + OPFS or memory) |
| describeEnvironment() | { crossOriginIsolated, opfsAvailable, recommendedMode } |
| headersPresets() | COOP/COEP maps for servers |
| SqliteWasmError | Normalized errors from the worker |
| sqlite-promiser/worker | OO1 worker script URL (for Worker or require.resolve) |
| sqlite-promiser/sqlite3.wasm | WASM bytes path |
| sqlite-promiser/opfs-async-proxy | OPFS async helper worker script (sibling of OO1 worker) |
| sqlitePromiserDevPlugin() (sqlite-promiser/vite) | Optional Vite dev/preview COOP + origin helpers |
Database: exec, run, all, get, transaction, export, close, getDiagnostics.
Options
name— logical DB name (sanitized for filenames).preferOpfs— defaulttrue.fallback—'memory'(default; IndexedDB planned separately).vfs— optional VFS override for the workeropenstep (advanced).worker— optional customWorkerfactory; must still load the published OO1 RPC bundle (sqlite-promiser/workeror equivalent) so message shapes match. Use this when your bundler should own worker URL resolution (e.g. Vitenew URL('sqlite-promiser/worker', import.meta.url)).
Testing this repo
pnpm --filter sqlite-promiser test
pnpm exec playwright install chromium # once per machine / CI image
pnpm test:e2e # from repo rootLicense
MIT (this wrapper). @sqlite.org/sqlite-wasm is Apache-2.0 — see upstream.
