Getting started
smaltdb is an embedded SQL database for web pages. The engine runs in a Web Worker, stores data in the Origin Private File System (OPFS), and speaks a SQL dialect based on SQLite. Your page talks to it through a small promise-based client.
Install
npm install smaltdbThe package has three entry points:
| Import | File | Use it for |
|---|---|---|
smaltdb | dist/smaltdb.js | The client: open() a database from a page or a worker. The engine runs in its own worker. |
smaltdb/engine | dist/engine.js | The engine itself, synchronous and in-process: Node, tests, tools, or inside your own worker. See the engine API. |
smaltdb/worker | dist/smaltdb-worker.js | The default engine worker script (no plugins), for setups that need its path (see below). |
smaltdb/worker-runtime | dist/worker-runtime.js | serve(), to write your own engine worker entry with plugins. |
The package has no runtime dependencies. Optional features are separate plugin packages: @smaltdb/io (CSV and JSON formats, import and export), @smaltdb/external (files, URLs, S3 and Azure as data sources) and @smaltdb/parquet (Parquet).
Type declarations ship with the package. The engine needs Node 20 or later when you run it in Node.
A first query
import { open } from 'smaltdb';
const db = await open('app'); // a database named "app", stored in OPFS
await db.exec('CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT NOT NULL)');
await db.exec('INSERT INTO notes(body) VALUES (?)', ['Hello, smaltdb']);
const rows = await db.query('SELECT id, body FROM notes');
console.log(rows); // [{ id: 1, body: 'Hello, smaltdb' }]
await db.close();open() starts the engine worker and opens (or creates) the database. Every call returns a promise, so the main thread never waits on the database. The same SQL runs in the engine directly:
CREATE TABLE notes(id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes(body) VALUES ('Hello, smaltdb');
SELECT id, body FROM notes;| id | body |
|---|---|
| 1 | Hello, smaltdb |
Next: the quick start builds a small data layer step by step.
Loading the engine worker
The client starts the engine with this line, in dist/smaltdb.js:
new Worker(new URL('./smaltdb-worker.js', import.meta.url), { type: 'module', name: 'smaltdb' });So the worker is found next to the module that contains the client code. The files it needs are:
node_modules/smaltdb/dist/
smaltdb.js the client
smaltdb-worker.js the engine worker (a module worker)How this works with your build:
- Vite. Works without configuration. Vite recognises the
new Worker(new URL(…, import.meta.url))pattern and emits the worker as an asset, in the dev server and in production builds (checked with Vite 8). A production build may warn thatnode:fs/promises"has been externalized for browser compatibility": that import is only used when the engine runs in Node, and the warning can be ignored. - No bundler. If the page loads
dist/smaltdb.jsas an ES module from where the package files are served, the worker URL resolves todist/smaltdb-worker.jsnext to it. - Other bundlers. A bundler that understands the same pattern emits the worker the same way. If yours does not, or it moves the client code so the relative URL no longer points at the worker, copy
dist/smaltdb-worker.jsto your site and pass the worker's URL with theworkeroption.
The worker option overrides where the engine worker comes from:
import { open } from 'smaltdb';
// A URL or string: the client starts `new Worker(url, { type: 'module' })`.
const db = await open('app', { worker: new URL('/smaltdb-worker.js', location.href) });You can also pass a function that starts the worker, which is how you load a worker entry with plugins, or a Worker you created yourself. Prefer a function or a URL: with multi-tab access, a tab that takes over the database later starts its engine worker from the option, and a Worker object can only be used once.
Browser requirements
What each feature needs from the browser, from the client and worker code:
| Feature | Needs | Without it |
|---|---|---|
| The engine | Module workers (new Worker(url, { type: 'module' })) | smaltdb does not run. |
Persistent storage (storage: 'opfs', the default) | OPFS with FileSystemSyncAccessHandle in dedicated workers | open() fails with SMALT_CANTOPEN. Use storage: 'memory', which needs no OPFS. |
| Sharing one database between tabs | Web Locks (navigator.locks) and BroadcastChannel | Each open() owns the file alone; a second tab that opens it fails with SMALT_BUSY. |
Stopping a statement that is already running (signal) | A cross-origin isolated page (crossOriginIsolated and SharedArrayBuffer) | Aborting still removes queued requests and closes streams, but a running statement runs to the end. See cancellation. |
| Parallel reader workers | Multi-tab access, OPFS sync handles with mode: 'readwrite-unsafe', and more than one CPU core | Reads run on the owning engine worker. |
A page is cross-origin isolated when it is served with these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpStorage, transactions and multi-tab access work without them.
The engine is designed for Chrome 108, Firefox 111 and Safari 17 and later (ADR 0002). The project's browser test suite runs in Chromium by default. readwrite-unsafe handles, which parallel readers need, are a Chromium feature.
Node and other in-process use
smaltdb/engine is the same engine without the worker and the client: a synchronous API that runs in the current thread.
import { Database } from 'smaltdb/engine';
const db = Database.openMemory();
db.exec('CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT)');
db.run('INSERT INTO t(b) VALUES (?)', ['x']);
console.log(db.query('SELECT a, b FROM t')); // { columns: ['a', 'b'], rows: [[1, 'x']] }
db.close();Storage in Node:
Database.openMemory()gives an in-memory database.Database.open(new MemoryVfs(), 'app.db')gives a file-format database whose files live in theMemoryVfsobject, in process memory.OpfsVfsneeds OPFS, which Node does not have: opening a file with it rejects withSMALT_CANTOPEN.- smaltdb ships no VFS for the Node file system. To persist to disk, implement the
Vfsinterface (see the engine API).
External data and import and export need plugins, in Node as in the browser. In Node, pass them when you open the database: Database.openMemory({ plugins: [io, external, parquet] }). See plugins.
Next steps
- Quick start: a short tutorial.
- Databases: storage, options, durability, multi-tab access.
- Queries: parameters, result shapes, streaming, values.
- Client API reference.