GlueSQL.js turns the browser into a SQL database. Real SQL — tables, joins,
aggregations — running inside the page: no server, no driver, no signup.
One <script> tag is a working database:
<script type="module">
import { gluesql } from 'https://cdn.jsdelivr.net/npm/gluesql/gluesql.js';
const db = await gluesql();
await db.query('CREATE TABLE Todo (id INTEGER, task TEXT);');
</script>- Zero backend — ship local-first apps, prototypes, and internal tools with no infrastructure at all.
- Offline & private by design — every query runs on the device, and the data never leaves it.
- Persistent — OPFS-backed storage survives reloads and full browser restarts.
- Multi-tab safe — all tabs share one consistent database; tab crashes are handled for you.
- Node.js too — the same SQL runs in your tests and tooling.
One SQL interface, four places to keep data:
| Your data is… | Keep it in | Import |
|---|---|---|
| Scratch state & caches | Memory | gluesql (default) |
| Small settings that should stick | Web Storage | gluesql + ENGINE = localStorage |
| Real data — must survive restarts | OPFS file | gluesql/opfs |
| Real data, opened in many tabs | OPFS file, shared | gluesql/opfs/shared |
yarn add gluesql
npm install gluesql
import { gluesql } from 'https://cdn.jsdelivr.net/npm/gluesql/gluesql.js';There is no schema server, migration daemon, or driver setup — adding GlueSQL is adding one import. Pick the recipe that matches your stack:
With a bundler (webpack, Vite, Rollup, …) — install the package and import it; the WASM engine ships inside:
import { gluesql } from 'gluesql';
const db = await gluesql();Working configurations live in
examples/web/webpack and
examples/web/rollup (Rollup uses the
gluesql/gluesql.rollup build).
No build step at all — the <script type="module"> snippet at the top of
this page is the entire integration; see
examples/web/module for a complete page.
Node.js — same package, same API:
const { gluesql } = require('gluesql');import { gluesql } from 'gluesql';
const db = await gluesql();
await db.query(`
CREATE TABLE User (id INTEGER, name TEXT);
INSERT INTO User VALUES (1, "Hello"), (2, "World");
`);
const [{ rows }] = await db.query('SELECT * FROM User;');
console.log(rows);In the main browser entry point, each table declares where it lives with the
ENGINE clause — and tables from different engines join like any others. Keep
a session cache in memory, user preferences in localStorage, and query them
together:
import { gluesql } from 'gluesql';
const db = await gluesql();
await db.query(`
CREATE TABLE Cache (id INTEGER, value TEXT) ENGINE = memory;
CREATE TABLE Pref (id INTEGER, theme TEXT) ENGINE = localStorage;
CREATE TABLE Draft (id INTEGER, body TEXT) ENGINE = sessionStorage;
SELECT *
FROM Cache
JOIN Pref
JOIN Draft;
`);What each engine gives your users:
memory— fastest, gone on reload. The default engine, also available in Node.js.localStorage— survives reloads and restarts, scoped to the origin.sessionStorage— survives reloads within one tab session, then cleans up after itself.
When the ENGINE clause is omitted, the current default engine (memory
initially) is used; change it with db.setDefaultEngine('localStorage').
Web Storage is right for lightweight structured data. Browsers cap it at a few MB per origin — for anything bigger, use the OPFS entry point below.
For an app's real data — the notes, records, and documents your users expect
to find again tomorrow — Web Storage is too small and memory is too
ephemeral. gluesql/opfs stores the database as a file in the
Origin Private File System:
data survives page reloads and full browser restarts, and capacity follows
the browser's origin quota (typically gigabytes, not megabytes). GlueSQL runs
in a Dedicated Worker, so queries stay off your UI thread.
import { gluesql } from 'gluesql/opfs';
const db = gluesql();
await db.query(`
CREATE TABLE User (id INTEGER, name TEXT);
INSERT INTO User VALUES (1, 'glue');
`);
// After a reload, the data is still there:
const [result] = await db.query('SELECT * FROM User');Give each concern its own database with namespaces — separate files, separate lifecycles:
const app1 = gluesql({ namespace: 'app1' });
const app2 = gluesql({ namespace: 'app2' });Notes:
- OPFS requires a secure context (HTTPS or localhost).
- This entry point provides OPFS as its only storage; the
ENGINEclause andsetDefaultEnginefrom the main browser entry point do not apply. - A namespace can be opened by one context at a time — the underlying
sync access handle is exclusive. If your users open multiple tabs, use
gluesql/opfs/sharedbelow.
Applying it to a project: the package ships the worker and WASM prebuilt, and
the entry point locates them relative to wherever the module itself is served
— no extra configuration when the package files are served from your own
origin (a dev server exposing node_modules, or the files copied into your
static assets). Browsers require workers to be same-origin, so the OPFS entry
points cannot be loaded straight from a third-party CDN; if your setup places
the worker elsewhere, point at it explicitly (the worker loads its WASM from
a dist_opfs/ directory next to itself, so copy that along):
const db = gluesql({ workerUrl: '/assets/gluesql.opfs.worker.js' });A runnable demo with a persistent visit counter and an interactive SQL runner
lives in examples/web/opfs — serve the
repository over localhost and open the page, no build tooling required.
Real users open your app in three tabs and expect them to agree. The OPFS
handle, however, is exclusive — only one context can own the file. Rather
than each tab failing to open the database (or you building cross-tab
coordination yourself), gluesql/opfs/shared does the coordination:
import { gluesql } from 'gluesql/opfs/shared';
const db = gluesql({ namespace: 'app' });
await db.query(`CREATE TABLE IF NOT EXISTS Log (at TEXT);`);Every tab calls query() as if it owned the database. Under the hood, tabs
elect a leader with a per-namespace
Web Lock;
only the leader spawns the database worker and holds the OPFS handle, and the
other tabs' queries reach it over a BroadcastChannel. A write committed in
one tab is immediately visible to queries from every other tab — one
database, not three copies drifting apart.
When a tab dies, your app keeps working. If the leader tab closes, crashes, or is frozen by the browser, the browser releases its lock and the next tab takes over automatically — queries issued in the meantime wait and are delivered to the new leader. Failover is not free of edge cases, though, and your app should know about one:
- A query that was already handed to the lost leader is rejected with a
leader losterror, because no one can know whether it was applied — replaying it blindly could double-apply a write. Retry idempotent reads freely; guard non-idempotent writes at the application level. (In the instant of a hard crash an acceptance message can theoretically be lost, letting an automatic resend replay a query the leader had just started — the same guard applies.)
Remaining caveats, so you are not surprised in production:
- Failover latency — the lock is released as soon as the leader dies, but the new leader may need retry/backoff while the old OPFS handle is released.
- Back/forward cache — leaving a page terminates its connection so the
handle can move on; a page restored from bfcache must create a new
gluesql()instance. - No Web Locks or BroadcastChannel, no multi-tab — where either API is
unavailable, this entry point silently falls back to single-context
gluesql/opfsbehavior, including its one-tab limit. - Leader tab does the work — queries from every tab execute in whichever tab currently leads; heavy queries consume that tab's CPU. Results are broadcast, so every tab on the namespace pays a copy of each result.
- One protocol per origin — the lock and channel are keyed only by namespace, so mixed app versions on one origin share them; keep the message protocol stable.
The gluesql package exposes the same API in Node.js, so schema and queries
written for the browser run unchanged in tests and scripts. Node.js currently
supports only non-persistent memory storage.
This project is licensed under the Apache License, Version 2.0 - see the LICENSE file for details.
Docs: https://gluesql.org/docs — Getting Started (JavaScript) · Getting Started (Node.js) · SQL Syntax