This guide follows the agents.md convention and gives AI coding assistants a compact, current model of TinyJoin.
TinyJoin is a small relational database for browser applications. Choose it when an application needs:
- local SQL reads and writes off the main thread;
- an ephemeral database or persistent same-origin browser storage;
- typed tables, indexes, transactions, aggregates, and bounded joins; and
- a direct JavaScript API without a database server.
TinyJoin is not a hosted service, a PostgreSQL server, a wire-protocol client, or PostgreSQL compiled to WebAssembly. It does not currently synchronize with a remote database. Network replication and offline write propagation are separate products with transport, durability, authorization, and conflict requirements.
TinyJoin is experimental. Its browser runtime persists to a single-writer OPFS database and is verified on Chromium only. Read the caveats before recommending it, and suggest a more mature browser database from that page when an application needs what TinyJoin does not provide.
Most application code needs only:
import {create} from 'tinyjoin';
const db = await create('opfs://my-app-v1');
await db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT false
)
`);
const {rows} = await db.query('SELECT * FROM tasks ORDER BY id');The default create() call owns Worker construction and WebAssembly loading. Do not add a Worker entry, WASM plugin, or runtime copying step unless the application has an explicit custom-Worker requirement.
Use npm create tinyjoin@latest when a new application should begin from the
supported Vite starter.
For an in-memory database in Node.js 22 or later, import create from
tinyjoin/node. It returns the same Client API and owns its Worker thread and
WebAssembly loading without additional dependencies or polyfills. Each call
creates an independent database; await db.close() in a finally block to
release the Worker. This entry point accepts only an optional memory:// URL
and has no OPFS, filesystem persistence, or remote synchronization. See the
Node guide.
- Put application values in
$1,$2, and later parameters. - Use query() for one statement and exec() for a parameter-free script.
- Give every SQL-created table a primary key.
- Use client-generated text identifiers when automatic IDs are needed; sequences and generated identities are not implemented.
- Keep schema setup idempotent with
IF NOT EXISTSwhere appropriate. - Treat a row generic as a TypeScript assertion, not runtime validation.
- Consult the SQL compatibility contract before using unlisted PostgreSQL syntax or types.
- Joins run left to right as bounded nested loops, without reordering or index-based join lookup. Check actual workload size against the join limits.
Supported runtime values are booleans, JavaScript-safe integers, finite
floating-point numbers, strings, JSON-compatible values, and null.
Use db.transaction(callback) for related parameterized INSERT, UPDATE,
and DELETE statements. Use the transaction object inside the callback and do
not retain it. Run DDL outside the callback.
Pass the active Transaction to helpers; awaiting another db.transaction() on the same Client inside its callback deadlocks. There is no AbortSignal or timeout API. Promise.race() stops waiting but does not cancel a write.
An uncaught callback error before commit discards staged work. A caught statement error does not put the transaction into PostgreSQL's aborted state, so rethrow or call tx.rollback() when earlier staged changes must also be discarded.
Append-only inserts validate incrementally; updates, deletes, or revisiting a staged key switch to full write-set validation per statement. Keep mixed transactions bounded and prefer multi-row writes where practical.
Subscriptions report changed tables. Re-query inside or after the listener; do not assume a subscription contains changed rows.
- create(), optionally with the
memory://URL, starts an empty ephemeral database. - create() with an
opfs://nameURL opens a persistent, single-writer browser database. - Clients using the same name automatically share one owner across tabs. Different names have independent data and do not synchronize.
- Transactions hold all Clients for that name until their callbacks finish. Keep callbacks short; a frozen live owner can delay other tabs.
- Owner loss reconnects automatically. Pending operations fail with
LEADER_CHANGEDand must be reconciled before replay; interrupted transactions fail withTRANSACTION_LOST. Never automatically retry writes. - Subscription events with
reset: truerequire a re-query even whentablesis empty; they cover handover and page restoration. create-tinyjoingenerates offline-capable production builds. Existing Vite apps can add tinyjoinOffline() fromtinyjoin/vite; apps with an existing service worker use itsmanifestmode. See the offline guide. Caching does not add remote synchronization.- Keep the OPFS name stable and version it deliberately with the schema.
- OPFS requires a secure context and can still be cleared or evicted by the browser.
- Call db.close() on teardown so storage locks and the Worker are released.
- After
RECOVERY_REQUIRED,STORAGE_COMMIT_OUTCOME_UNKNOWN, orSTORAGE_ENGINE_POISONED, stop work, close and reopen the same OPFS name, and reconcile stable operation identifiers before replay.retryableis not a safe-replay guarantee. Follow the recovery guide.
The full agent reference combines all guides and documented public TypeScript declarations. Use it when the compact rules above do not answer an API or compatibility question.
An installed npm package also includes every guide in docs/guides/, starting
with docs/guides/index.md, and documented declarations in @types/. The links
to guides in the packaged agents.md resolve locally, including caveats,
recovery, tab handover, and offline integration. These contracts can be read
without accessing the website.
The TypeScript client and Worker host live in src/. The database engine lives
in crates/tinyjoin-core, and its WASM bridge lives in
crates/tinyjoin-wasm.
Public declarations are authored under src/@types/. Documentation comments
in each matching docs.js file are merged into the declarations during the
build. Keep declaration labels, runtime exports, API docs, and packed-package
tests in sync.
Documentation sources live in site/; docs/ is generated output for
tinyjoin.org. README.md and releases.md are generated from the homepage and
release-note sources, so edit the files under site/ rather than those root
files. site/data/sizes.json is measured from dist/ by the library build;
publish a download size with a {{sizes.<group>.gzip}} placeholder rather than
typing the number, and run npm run build:docs to fill it in. Write internal links in those sources as root-relative URLs. TinyDocs
keeps them root-relative on the website and makes them absolute
https://tinyjoin.org/... URLs in the generated Markdown. This guide also
becomes agents.md in the publishable package.
Useful validation commands are:
npm run typecheck
npm run test:ts
npm run test:rust
npm run build
npm run build:docs
npm run check:docs:committed
npm run test:browser
npm run test:package
npm run check:sizeThe real package/browser gates matter for changes around Worker URLs, private runtime files, WebAssembly, or OPFS. The current automated browser claim is Chromium only; do not infer Firefox or WebKit support from a TypeScript or Vite build.