Skip to main content

@monlite/wasm — monlite in the browser

Run monlite in the browser on SQLite-WASM (sql.js) through a custom Driver. The whole document / query / structured / transaction API is identical to the server — only the driver changes. The same .db bytes move between the browser and Node.

npm install @monlite/wasm sql.js
import initSqlJs from "sql.js";
import { createDb } from "@monlite/core";
import { wasmDriver, exportDatabase } from "@monlite/wasm";

// 1. Initialise sql.js yourself (async), then build the driver (sync).
const SQL = await initSqlJs({ locateFile: (f) => `/sql-wasm/${f}` });
const db = createDb(":memory:", { driver: wasmDriver(SQL) });

// 2. Use monlite exactly as on the server.
await db.collection("notes").create({ data: { text: "hello from the browser" } });
const notes = await db.collection("notes").findMany({ where: { text: { contains: "hello" } } });

// 3. Persist a snapshot and reopen it later.
const bytes = exportDatabase(db); // Uint8Array
const db2 = createDb(":memory:", { driver: wasmDriver(SQL, { data: bytes }) });

createDb always takes ":memory:" here — sql.js itself is in-memory, so the file path is unused; the driver holds the database. Persistence is explicit (see below).

API

wasmDriver(SQL, options?) // build a Driver from an initialised sql.js module
exportDatabase(db) // Uint8Array — serialize the whole database
new WasmDriver(SQL, options?) // the class behind wasmDriver()

WasmDriverOptions

OptionMeaning
dataexisting database bytes to open (e.g. restored from IndexedDB / OPFS)
onQuery({ sql, durationMs }) => void — called after every statement; mirrors createDb's onQuery, which doesn't reach a user-supplied driver. Useful for an in-browser query log.

The driver also exposes a better-sqlite3-compatible escape hatch as db.sqlite (prepare / exec / run / export / create_function, plus raw for the underlying sql.js handle) so the FTS, vector, and kv plugins work unchanged.

Persistence (IndexedDB / OPFS)

sql.js keeps the database in memory; nothing is saved until you serialize it with exportDatabase(db) and write the bytes somewhere durable. On startup, read them back and pass them as { data }.

// Save — call after writes (debounce in a real app), e.g. to IndexedDB via idb-keyval:
import { set, get } from "idb-keyval";
await set("app.db", exportDatabase(db));

// Restore on next load:
const saved = (await get("app.db")) as Uint8Array | undefined;
const db = createDb(":memory:", { driver: wasmDriver(SQL, { data: saved ?? null }) });

OPFS works the same way — write the Uint8Array to an OPFS file with a FileSystemWritableFileStream, and read it back on load. Either way the unit of persistence is the whole database snapshot, so debounce saves on bursty write workloads.

Bundling for the browser

@monlite/core loads the native Node drivers via node:module, which only runs on the Node path — but a bundler still needs it stubbed for a browser build. Alias these in your bundler (see the demo's vite.config.ts):

// vite.config.ts → resolve.alias
"node:module": "/path/to/empty-stub.js", // @monlite/core native-driver loader
"sqlite-vec": "/path/to/empty-stub.js", // @monlite/vector (uses its JS fallback)
"events": "/path/to/events-shim.js", // @monlite/queue & cron (Node EventEmitter)

What works in the browser

@monlite/core (documents, queries, structured collections, transactions, watch()), plus @monlite/kv, @monlite/queue, and @monlite/cron, all run on the WASM driver unchanged. Two plugins need attention:

Full-text search (FTS5)

@monlite/fts works in the browser — but the default sql.js build ships without the FTS5 extension (CREATE VIRTUAL TABLE … USING fts5 fails with "no such module: fts5"). Use a SQLite-WASM build compiled with FTS5, such as fts5-sql-bundle — a drop-in for sql.js with the same initSqlJs API:

import initSqlJs from "fts5-sql-bundle/dist/sql-wasm.js";
import { createDb } from "@monlite/core";
import { wasmDriver } from "@monlite/wasm";
import { fts } from "@monlite/fts";

const SQL = await initSqlJs();
const db = createDb(":memory:", {
driver: wasmDriver(SQL),
plugins: [fts({ docs: ["title", "body"] })],
});
await db.collection("docs").search("hello world"); // FTS5, in the browser

@monlite/vector (0.5.0+) works in the browser too. When the native vec0 extension (sqlite-vec) can't be loaded — as in SQLite-WASM — findSimilar() transparently falls back to an exact brute-force JS implementation (cosine / L2 over a plain table). No config; same API:

import { vector } from "@monlite/vector";

const db = createDb(":memory:", {
driver: wasmDriver(SQL),
plugins: [vector({ docs: { field: "embedding", dimensions: 384, distance: "cosine" } })],
});
await db.collection("docs").findSimilar({ vector: queryEmbedding, topK: 5 });

The fallback is O(n) per query (exact, not approximate) — right for the thousands-of-vectors scale a local / edge store holds. Alias the native sqlite-vec module to a stub in your bundle (it's only used on the Node path).

Limits

  • In-memory. No automatic persistence — you serialize and store snapshots yourself (above). Reads and writes are fast, but a large database lives entirely in browser memory.
  • One writer. A single sql.js instance per tab; there's no cross-tab shared connection. Use a SharedWorker, or serialize through one tab, if multiple tabs must write.
  • Extensions (FTS5, sqlite-vec) depend on the WASM build — FTS5 needs a bundle that includes it; vector uses the JS fallback.

The live demo runs exactly this — an in-browser document store with FTS5 search and on-device embeddings (via Transformers.js), no server. Use this for offline-first web apps, local-first PWAs, and in-browser demos.