TinyJoin logoTinyJoin

API

The Essentials

create

Essential

The create function opens a TinyJoin database in a dedicated Worker.

create(): Promise<Client>
returnsPromise<Client>

A Promise resolving to a Client whose Worker and database are ready.

It constructs a Client and resolves once that Client is ready, so a database that cannot be opened rejects here. The Client constructor returns before then, for code that needs the waitReady lifecycle.

The overload of this function with no arguments creates an ephemeral in-memory database using the packaged Worker. Its data is lost when the Client closes or the page reloads.

Example

import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL
  )
`);
const {rows} = await db.query('SELECT * FROM tasks ORDER BY id');
await db.close();

The overload of this function with one argument, an options object, opens the database that its ClientOptions describe.

create(options: ClientOptions): Promise<Client>
TypeDescription
optionsClientOptions

Storage and Worker configuration.

returnsPromise<Client>

A Promise resolving to a Client whose Worker and database are ready.

Use the options object to select storage or provide a custom Worker. Omitting its dataDir property selects an ephemeral in-memory database.

Example

import {create} from 'tinyjoin';

const db = await create({dataDir: 'memory://'});
await db.close();

The overload of this function with a storage URL as its first argument, and optionally options as its second, opens a database at that URL.

create(
  dataDir: string | undefined,
  options?: ClientOptions,
): Promise<Client>
TypeDescription
dataDirstring | undefined

The memory:// URL, a named opfs:// URL, or undefined for the default in-memory database.

options?ClientOptions

Optional Worker configuration.

returnsPromise<Client>

A Promise resolving to a Client whose Worker and database are ready.

Use a stable opfs://name to persist data in the same browser, or memory:// for an ephemeral database. When providing the second argument, supply a storage URL and leave options.dataDir unset.

Example

import {create} from 'tinyjoin';

const db = await create('opfs://my-app');
await db.close();

exec

Essential

The exec method runs one or more parameter-free statements as one implicit transaction.

exec(
  sql: string,
  options?: QueryOptions,
): Promise<Results<Row>[]>
TypeDescription
sqlstring
options?QueryOptions
returnsPromise<Results<Row>[]>

A Promise resolving to one result per statement, in script order.

Example

import {create} from 'tinyjoin';

const db = await create();
const results = await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL);
  INSERT INTO tasks (id, title) VALUES ('a', 'Write docs');
  SELECT * FROM tasks;
`);
console.log(results.length);
// -> 3
console.log(results[2].rows);
// -> [{id: 'a', title: 'Write docs'}]
await db.close();

query

Essential

The query method executes exactly one parameterized SQL statement.

query<RowType>(
  sql: string,
  params?: JsonValue[],
  options?: QueryOptions,
): Promise<Results<RowType>>
TypeDescription
sqlstring

A statement in the documented TinyJoin SQL subset.

params?JsonValue[]

JSON-compatible values for $1, $2, and so on.

options?QueryOptions

Result-shape options.

returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Examples

import {create} from 'tinyjoin';

const db = await create();
await db.query(
  'CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)',
);
const inserted = await db.query(
  'INSERT INTO tasks (id, title) VALUES ($1, $2)',
  ['a', 'Write docs'],
);
console.log(inserted.affectedRows);
// -> 1

const {rows} = await db.query<{id: string; title: string}>(
  'SELECT * FROM tasks WHERE id = $1',
  ['a'],
);
console.log(rows);
// -> [{id: 'a', title: 'Write docs'}]
await db.close();
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL);
  INSERT INTO tasks (id, title) VALUES ('a', 'Write docs');
`);
// Rows come back as arrays, in the order of the fields, on request.
const {fields, rows} = await db.query('SELECT id, title FROM tasks', [], {
  rowMode: 'array',
});
console.log(fields.map(({name}) => name));
// -> ['id', 'title']
console.log(rows);
// -> [['a', 'Write docs']]
await db.close();

sql

Essential

The sql method is a parameterizing tagged-template form of query.

sql<RowType>(
  strings: TemplateStringsArray,
  ...params: JsonValue[],
): Promise<Results<RowType>>
TypeDescription
stringsTemplateStringsArray
...paramsJsonValue[]
returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Interpolated values become $n parameters. It does not interpolate raw identifiers or SQL fragments.

Example

import {create} from 'tinyjoin';

const db = await create();
await db.sql`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`;

const id = 'a';
const title = 'Write docs';
await db.sql`INSERT INTO tasks (id, title) VALUES (${id}, ${title})`;

const {rows} = await db.sql<{title: string}>`
  SELECT title FROM tasks WHERE id = ${id}
`;
console.log(rows);
// -> [{title: 'Write docs'}]
await db.close();

transaction

Essential

The transaction method stages row mutations and publishes them together when the callback succeeds, unless it explicitly rolls back.

transaction<Result>(callback: (transaction: Transaction) => Result | Promise<Result>): Promise<Result>
TypeDescription
callback(transaction: Transaction) => Result | Promise<Result>
returnsPromise<Result>

A Promise resolving to the callback's result after the transaction commits or explicitly rolls back.

Transaction calls on this Client queue in order. Do not await another transaction on this Client inside the callback; pass its Transaction to helpers instead. Use that object for all SQL in the callback and prepare handles beforehand. Other Clients for the same OPFS name wait for the whole callback to finish. Do not await work on those Clients from inside this callback. An uncaught callback error before commit discards staged work. A caught statement error leaves earlier writes staged unless rollback is called.

There is no cancellation or timeout option. Promise.race only stops waiting, and queued work can still commit. Rejection during commit can leave its outcome uncertain; see the recovery guide before replaying a failed write.

Example

import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE accounts (id TEXT PRIMARY KEY, balance INTEGER NOT NULL);
  INSERT INTO accounts (id, balance) VALUES ('a', 100);
  INSERT INTO accounts (id, balance) VALUES ('b', 0);
`);
// Both updates commit together, or neither does.
const moved = await db.transaction(async (tx) => {
  await tx.query(
    'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
    [30, 'a'],
  );
  await tx.query(
    'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
    [30, 'b'],
  );
  return 30;
});
console.log(moved);
// -> 30

const {rows} = await db.query('SELECT * FROM accounts ORDER BY id');
console.log(rows);
// -> [{id: 'a', balance: 70}, {id: 'b', balance: 30}]
await db.close();

tinyjoin

The tinyjoin module provides a small PostgreSQL-shaped relational database that runs in a dedicated browser Worker and stores data in memory or OPFS.

Start with the create function. It constructs the Worker and loads the WebAssembly engine, so applications do not need to manage either directly.

Classes

Lifecycle classes

Client

The Client class represents one open TinyJoin database and its dedicated Worker.

Prefer the async create function so initialization failures are reported before the Client is returned.

Constructors
constructor

The constructor begins opening a Client immediately, and returns before the Client is ready.

new Client(options?: ClientOptions): Client
TypeDescription
options?ClientOptions
returnsClient

Prefer the create function, which constructs a Client and resolves once it is ready, unless code specifically needs the waitReady lifecycle.

Example
import {Client} from 'tinyjoin';

const db = new Client({dataDir: 'opfs://my-app-v1'});
console.log(db.ready);
// -> false

await db.waitReady;
console.log(db.ready);
// -> true
await db.close();
Methods
Lifecycle methods
check

The check method reads every row and index entry of the committed database, and every page that holds them, and resolves when all of them are sound.

check(): Promise<void>

Opening a database checks only its catalog, and each page and row is checked as a statement reads it, so rows that are never read, and whether each index still matches its table, are checked only here. The check takes time in proportion to the database, and statements from every Client of the same OPFS name wait while it runs. Call it outside a transaction.

TinyJoin cannot yet repair a database that fails its check.

Example
import {ClientError, create} from 'tinyjoin';

const db = await create('opfs://my-app-v1');
try {
  await db.check();
  console.log('Every row and index entry is sound.');
} catch (error) {
  if (!(error instanceof ClientError)) {
    throw error;
  }
  // Nothing repairs a database that fails its check, so the application
  // restores its own backup into a new OPFS name.
  console.error(error.code);
} finally {
  await db.close();
}
close

The close method detaches this Client and releases its prepared statements and Worker. Other Clients for the same OPFS name stay connected; ownership transfers automatically when necessary. Repeated calls share the same asynchronous cleanup. A closed Client cannot resume; create a new one and recreate its prepared statements and subscriptions. Closing is not a cancellation or rollback guarantee for a write already in flight.

close(): Promise<void>
Example
import {create} from 'tinyjoin';

const db = await create('opfs://my-app-v1');
try {
  await db.exec(`
    CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY)
  `);
} finally {
  // Releases the Worker and its hold on the storage, whatever happened.
  await db.close();
}
console.log(db.closed);
// -> true
getSchema

The getSchema method reads the database's tables, with their columns, primary keys, and indexes, as the engine's catalog holds them.

getSchema(): Promise<Schema>
returnsPromise<Schema>

A Promise resolving to the Schema.

Tables and each table's indexes are in name order, and columns and key columns are in the order the table declares them. Call it outside a transaction.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, done BOOLEAN NOT NULL DEFAULT false);
  CREATE INDEX tasks_done ON tasks (done);
`);
const {tables} = await db.getSchema();
console.log(tables[0].columns[1]);
// -> {name: 'done', type: 'boolean', nullable: false, default: false}
await db.close();
setSchema

The setSchema method makes the database's tables, columns, primary keys, and indexes match a Schema, as getSchema returns one, in one change that commits whole or not at all.

setSchema(
  schema: Schema,
  options?: SetSchemaOptions,
): Promise<boolean>
TypeDescription
schemaSchema

The schema to make the database's.

options?SetSchemaOptions

Whether to drop what the schema leaves out.

returnsPromise<boolean>

A Promise resolving to true if the database changed, or false if it already had the schema.

It creates the tables and columns the schema has and the database lacks, renames a table or column from the name its renamedFrom gives while the database still has that name, and changes a column's default, nullability, or VARCHAR length to the schema's. Each table's indexes and foreign keys become exactly the schema's, with keys added once every table they reference exists. Tables and columns the schema leaves out stay, unless the options' drop property is true. Rows keep their values, as ALTER TABLE keeps them: a change no statement could make, to a primary key or a column's runtime type, is refused, and so is one that a row prevents, such as NOT NULL where a row holds NULL.

A schema the database already has changes nothing, so every tab of an application can call setSchema as it starts. A schema whose version is lower than the database's is refused with SCHEMA_OUTDATED, so that a tab still running an older copy of the application cannot undo a newer one's changes; a higher version is recorded. Call it outside a transaction.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.setSchema({
  version: 1,
  tables: [
    {
      name: 'tasks',
      columns: [
        {name: 'id', type: 'text', nullable: false},
        {name: 'title', type: 'text', nullable: false},
        {name: 'done', type: 'boolean', nullable: false, default: false},
      ],
      primaryKey: ['id'],
      indexes: [{name: 'tasks_done', columns: ['done'], unique: false}],
      foreignKeys: [],
    },
  ],
});
await db.close();
SQL methods
exec
Essential

The exec method runs one or more parameter-free statements as one implicit transaction.

exec(
  sql: string,
  options?: QueryOptions,
): Promise<Results<Row>[]>
TypeDescription
sqlstring
options?QueryOptions
returnsPromise<Results<Row>[]>

A Promise resolving to one result per statement, in script order.

Example
import {create} from 'tinyjoin';

const db = await create();
const results = await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL);
  INSERT INTO tasks (id, title) VALUES ('a', 'Write docs');
  SELECT * FROM tasks;
`);
console.log(results.length);
// -> 3
console.log(results[2].rows);
// -> [{id: 'a', title: 'Write docs'}]
await db.close();
prepare

The prepare method parses and retains one reusable read or row-mutation statement in the Worker.

prepare<RowType>(sql: string): Promise<PreparedStatement<RowType>>
TypeDescription
sqlstring
returnsPromise<PreparedStatement<RowType>>

A Promise resolving to a reusable PreparedStatement handle.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL);
  INSERT INTO tasks (id, title) VALUES ('a', 'Write docs');
  INSERT INTO tasks (id, title) VALUES ('b', 'Ship release');
`);
const titleOf = await db.prepare<{title: string}>(
  'SELECT title FROM tasks WHERE id = $1',
);
for (const id of ['a', 'b']) {
  const {rows} = await titleOf.execute([id]);
  console.log(rows[0].title);
}
// -> 'Write docs'
// -> 'Ship release'
await titleOf.close();
await db.close();
query
Essential

The query method executes exactly one parameterized SQL statement.

query<RowType>(
  sql: string,
  params?: JsonValue[],
  options?: QueryOptions,
): Promise<Results<RowType>>
TypeDescription
sqlstring

A statement in the documented TinyJoin SQL subset.

params?JsonValue[]

JSON-compatible values for $1, $2, and so on.

options?QueryOptions

Result-shape options.

returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Examples
import {create} from 'tinyjoin';

const db = await create();
await db.query(
  'CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)',
);
const inserted = await db.query(
  'INSERT INTO tasks (id, title) VALUES ($1, $2)',
  ['a', 'Write docs'],
);
console.log(inserted.affectedRows);
// -> 1

const {rows} = await db.query<{id: string; title: string}>(
  'SELECT * FROM tasks WHERE id = $1',
  ['a'],
);
console.log(rows);
// -> [{id: 'a', title: 'Write docs'}]
await db.close();
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL);
  INSERT INTO tasks (id, title) VALUES ('a', 'Write docs');
`);
// Rows come back as arrays, in the order of the fields, on request.
const {fields, rows} = await db.query('SELECT id, title FROM tasks', [], {
  rowMode: 'array',
});
console.log(fields.map(({name}) => name));
// -> ['id', 'title']
console.log(rows);
// -> [['a', 'Write docs']]
await db.close();
sql
Essential

The sql method is a parameterizing tagged-template form of query.

sql<RowType>(
  strings: TemplateStringsArray,
  ...params: JsonValue[],
): Promise<Results<RowType>>
TypeDescription
stringsTemplateStringsArray
...paramsJsonValue[]
returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Interpolated values become $n parameters. It does not interpolate raw identifiers or SQL fragments.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.sql`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`;

const id = 'a';
const title = 'Write docs';
await db.sql`INSERT INTO tasks (id, title) VALUES (${id}, ${title})`;

const {rows} = await db.sql<{title: string}>`
  SELECT title FROM tasks WHERE id = ${id}
`;
console.log(rows);
// -> [{title: 'Write docs'}]
await db.close();
Transactions methods
transaction
Essential

The transaction method stages row mutations and publishes them together when the callback succeeds, unless it explicitly rolls back.

transaction<Result>(callback: (transaction: Transaction) => Result | Promise<Result>): Promise<Result>
TypeDescription
callback(transaction: Transaction) => Result | Promise<Result>
returnsPromise<Result>

A Promise resolving to the callback's result after the transaction commits or explicitly rolls back.

Transaction calls on this Client queue in order. Do not await another transaction on this Client inside the callback; pass its Transaction to helpers instead. Use that object for all SQL in the callback and prepare handles beforehand. Other Clients for the same OPFS name wait for the whole callback to finish. Do not await work on those Clients from inside this callback. An uncaught callback error before commit discards staged work. A caught statement error leaves earlier writes staged unless rollback is called.

There is no cancellation or timeout option. Promise.race only stops waiting, and queued work can still commit. Rejection during commit can leave its outcome uncertain; see the recovery guide before replaying a failed write.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE accounts (id TEXT PRIMARY KEY, balance INTEGER NOT NULL);
  INSERT INTO accounts (id, balance) VALUES ('a', 100);
  INSERT INTO accounts (id, balance) VALUES ('b', 0);
`);
// Both updates commit together, or neither does.
const moved = await db.transaction(async (tx) => {
  await tx.query(
    'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
    [30, 'a'],
  );
  await tx.query(
    'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
    [30, 'b'],
  );
  return 30;
});
console.log(moved);
// -> 30

const {rows} = await db.query('SELECT * FROM accounts ORDER BY id');
console.log(rows);
// -> [{id: 'a', balance: 70}, {id: 'b', balance: 30}]
await db.close();
Subscriptions methods
getRevision

The getRevision method returns the newest database revision observed by this Client.

getRevision(): number
returnsnumber

The newest database revision observed by this Client.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const before = db.getRevision();
const {revision} = await db.query(
  'INSERT INTO tasks (id, title) VALUES ($1, $2)',
  ['a', 'Write docs'],
);
console.log(revision > before);
// -> true
console.log(db.getRevision() == revision);
// -> true
await db.close();
subscribe

The subscribe method listens for committed table changes and returns an unsubscribe function. OPFS Clients receive changes from every connected tab. After handover or page restoration, reset events notify every subscriber to re-query even though tables is empty.

subscribe(
  options: SubscriptionOptions,
  listener: (event: TablesChangedEvent) => void,
): () => void
TypeDescription
optionsSubscriptionOptions
listener(event: TablesChangedEvent) => void
returns() => void

A function that removes this subscription when called.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const unsubscribe = db.subscribe({tables: ['tasks']}, async ({tables}) => {
  // The event names the tables that changed. Query again for their rows.
  const {rows} = await db.query('SELECT * FROM tasks ORDER BY id');
  console.log(tables, rows);
  // -> ['tasks'] [{id: 'a', title: 'Write docs'}]
  unsubscribe();
  await db.close();
});

await db.query('INSERT INTO tasks (id, title) VALUES ($1, $2)', [
  'a',
  'Write docs',
]);
Properties
closed

The closed property indicates that Client cleanup has completed. A terminal Worker failure does not itself complete Client cleanup. Call close even when subsequent operations reject with WORKER_TERMINATED.

boolean
ready

The ready property indicates that initialization finished and closing has not started. It is not a health check: it can remain true after a terminal Worker error while operations reject. Close and reopen the Client and reconcile writes; see the lifecycle guide. Temporary OPFS owner handover reconnects the same Client automatically.

boolean
waitReady

The waitReady property resolves when the Worker and database are ready. It settles once for initialization; it does not track later Worker health or temporary OPFS owner handover.

Promise<void>

Errors classes

ClientError

The ClientError class extends JavaScript Error with a validated error returned by the TinyJoin Worker.

RECOVERY_REQUIRED, STORAGE_COMMIT_OUTCOME_UNKNOWN, and STORAGE_ENGINE_POISONED mean the engine must no longer be used. Stop work, close the Client, reopen the same OPFS name, and reconcile stored state using stable operation identifiers before replaying a write. A rejected operation may already have committed. See the recovery guide.

Constructors
constructor

The constructor creates an Error from its serialized Worker envelope.

new ClientError(error: SerializedError): ClientError
TypeDescription
errorSerializedError
returnsClientError

Example
import {ClientError} from 'tinyjoin';

const error = new ClientError({
  code: 'INVALID_ARGUMENT',
  message: 'A task needs a title.',
  details: {column: 'title'},
});
console.log(error instanceof Error);
// -> true
console.log(error.code, error.details, error.retryable);
// -> 'INVALID_ARGUMENT' {column: 'title'} false
Properties
code

The code property identifies the TinyJoin error family.

string
details

The details property contains optional JSON-compatible context.

JsonValue | undefined
retryable

The retryable property indicates whether reopening or retrying may succeed. It is not a safe-replay guarantee. An uncertain write requires reopening and reconciliation even when application code wants to retry it.

boolean

Interfaces

SQL interfaces

PreparedStatement

The PreparedStatement interface represents one parsed SELECT, INSERT, UPDATE, or DELETE statement retained by an open Client.

Methods
Lifecycle methods
close

The close method seals the handle and releases its Worker resources after already-started executions settle.

close(): Promise<void>
Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const select = await db.prepare('SELECT * FROM tasks WHERE id = $1');
await select.execute(['a']);

await select.close();
console.log(select.closed);
// -> true
await db.close();
SQL methods
execute

The execute method binds a complete parameter list and runs the statement.

execute(
  params?: JsonValue[],
  options?: QueryOptions,
): Promise<Results<RowType>>
TypeDescription
params?JsonValue[]

JSON-compatible values for $1, $2, and so on.

options?QueryOptions

Result-shape options.

returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const insert = await db.prepare(
  'INSERT INTO tasks (id, title) VALUES ($1, $2)',
);
await insert.execute(['a', 'Write docs']);
await insert.execute(['b', 'Ship release']);

const {rows} = await db.query('SELECT title FROM tasks ORDER BY id');
console.log(rows);
// -> [{title: 'Write docs'}, {title: 'Ship release'}]
await insert.close();
await db.close();
Properties
closed

The closed property indicates whether the handle has been sealed. It is not a Worker health check. A terminal Worker failure can leave it false while execute rejects. Closing the Client seals its handles.

boolean

Transactions interfaces

Transaction

The Transaction interface runs reads and row mutations against one isolated staged database inside a Client.transaction callback.

Do not retain this object after its callback completes. Run schema DDL in a standalone Client.query call or Client.exec script. Pass this object to helpers instead of awaiting another transaction on the same Client, which would queue behind the active callback and deadlock.

Methods
exec

The exec method runs a parameter-free DML and read script as one savepoint.

exec(
  sql: string,
  options?: QueryOptions,
): Promise<Results<Row>[]>
TypeDescription
sqlstring
options?QueryOptions
returnsPromise<Results<Row>[]>

A Promise resolving to one result per statement, in script order.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
await db.transaction(async (tx) => {
  const results = await tx.exec(`
    INSERT INTO tasks (id, title) VALUES ('a', 'Write docs');
    INSERT INTO tasks (id, title) VALUES ('b', 'Ship release');
    SELECT id FROM tasks ORDER BY id;
  `);
  console.log(results[2].rows);
  // -> [{id: 'a'}, {id: 'b'}]
});
await db.close();
execute

The execute method runs a prepared statement owned by the same Client.

execute<RowType>(
  statement: PreparedStatement<RowType>,
  params?: JsonValue[],
  options?: QueryOptions,
): Promise<Results<RowType>>
TypeDescription
statementPreparedStatement<RowType>
params?JsonValue[]
options?QueryOptions
returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
// Prepare the statement before the transaction, and run it inside.
const insert = await db.prepare(
  'INSERT INTO tasks (id, title) VALUES ($1, $2)',
);
await db.transaction(async (tx) => {
  await tx.execute(insert, ['a', 'Write docs']);
  await tx.execute(insert, ['b', 'Ship release']);
});

const {rows} = await db.query('SELECT id FROM tasks ORDER BY id');
console.log(rows);
// -> [{id: 'a'}, {id: 'b'}]
await insert.close();
await db.close();
query

The query method runs one parameterized statement against staged data.

query<RowType>(
  sql: string,
  params?: JsonValue[],
  options?: QueryOptions,
): Promise<Results<RowType>>
TypeDescription
sqlstring
params?JsonValue[]
options?QueryOptions
returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
await db.transaction(async (tx) => {
  await tx.query('INSERT INTO tasks (id, title) VALUES ($1, $2)', [
    'a',
    'Write docs',
  ]);
  // A read inside the transaction sees the rows it has staged.
  const {rows} = await tx.query('SELECT title FROM tasks WHERE id = $1', [
    'a',
  ]);
  console.log(rows);
  // -> [{title: 'Write docs'}]
});
await db.close();
rollback

The rollback method explicitly discards the staged transaction.

rollback(): Promise<void>
Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
await db.transaction(async (tx) => {
  await tx.query('INSERT INTO tasks (id, title) VALUES ($1, $2)', [
    'a',
    'Write docs',
  ]);
  await tx.rollback();
  console.log(tx.closed);
  // -> true
});

const {rows} = await db.query('SELECT * FROM tasks');
console.log(rows);
// -> []
await db.close();
sql

The sql method is a parameterizing tagged-template form of query.

sql<RowType>(
  strings: TemplateStringsArray,
  ...params: JsonValue[],
): Promise<Results<RowType>>
TypeDescription
stringsTemplateStringsArray
...paramsJsonValue[]
returnsPromise<Results<RowType>>

A Promise resolving to the statement results.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const tasks = [
  {id: 'a', title: 'Write docs'},
  {id: 'b', title: 'Ship release'},
];
await db.transaction(async (tx) => {
  for (const {id, title} of tasks) {
    await tx.sql`INSERT INTO tasks (id, title) VALUES (${id}, ${title})`;
  }
});

const {rows} = await db.query('SELECT id FROM tasks ORDER BY id');
console.log(rows);
// -> [{id: 'a'}, {id: 'b'}]
await db.close();
Properties
closed

The closed property indicates that the transaction is sealed, including after an explicit rollback or after its callback finishes.

boolean

Query results interfaces

QueryOptions

The QueryOptions interface configures the shape of query results.

Only rowMode is supported. There is no AbortSignal or timeout option; Promise.race with a timer does not cancel database work.

rowMode

The rowMode property selects object rows by default, or arrays whose values follow the order of the fields property. Only array rows can return a SELECT whose output names repeat.

RowMode
ResultField

The ResultField interface describes one projected result column.

dataTypeID

The dataTypeID property contains the closest stable PostgreSQL OID for the TinyJoin runtime type.

number
name

The name property contains the projected column name or alias, or ?column? for an expression without an alias.

string
Results

The Results interface is returned by queries, prepared statements, and scripts.

It follows the familiar PGlite result shape and adds the observed database revision and the names of changed tables.

affectedRows

The affectedRows property reports the number of rows changed by a write.

number
command

The command property contains the executed statement family.

string
fields

The fields property describes the projected columns in result order.

ResultField[]
keys

The keys property contains the primary keys the statement changed, keyed by table name.

ChangedKeys

A table appears only when every key it changed fits the reporting bound of 1,000 keys per table. A table that changed more rows than that is absent, so an absent table means the rows that changed cannot be named, not that nothing changed. The tables property stays authoritative either way.

revision

The revision property contains the database revision observed by the statement.

number
rowCount

The rowCount property reports the number of rows returned or changed.

number
rows

The rows property contains the rows returned by the statement.

RowType[]
tables

The tables property contains the tables changed by the statement.

string[]

Subscriptions interfaces

SubscriptionOptions

The SubscriptionOptions interface filters invalidations by table name.

tables

The tables property limits delivery to invalidations that include at least one of these tables. Omit it to observe every table-change invalidation; adjacent changes may be coalesced.

string[]
TablesChangedEvent

The TablesChangedEvent interface describes one committed table-level invalidation or a request to refresh after database-owner handover.

keys

The keys property contains the primary keys that changed, keyed by table name, for the tables whose complete set is known.

ChangedKeys

Use it to refresh individual rows instead of re-reading a whole table. A table listed in tables but absent here changed more rows than the bound of 1,000 keys per table allows an event to name, or changed across a handover; re-query that table instead. It is always empty when reset is true.

reset

The reset property is true when database-owner handover or page restoration requires a re-query even though the changed tables are unknown. In that case tables is empty and every subscription is notified, including filtered ones. Normal committed-change events omit this property.

boolean
revision

The revision property contains the committed database revision.

number
tables

The tables property contains the names of changed tables. It is empty when reset is true, which means the subscriber should refresh its query anyway.

string[]

Configuration interfaces

ClientOptions

The ClientOptions interface configures storage or an application-owned Worker.

The zero-boilerplate default constructs TinyJoin's packaged module Worker. Provide at most one of worker, workerFactory, or workerUrl.

worker

The worker property provides an already-created Worker-compatible object.

WorkerLike
dataDir

The dataDir property selects memory or a named OPFS database.

string
workerFactory

The workerFactory property creates an application-owned Worker lazily.

() => WorkerLike
workerUrl

The workerUrl property identifies an application-owned module Worker.

string | URL

Errors interfaces

SerializedError

The SerializedError interface is the stable error envelope sent by the Worker.

code

The code property identifies the TinyJoin error family.

string
details

The details property contains optional JSON-compatible context.

JsonValue
message

The message property explains the failure.

string
retryable

The retryable property indicates whether reopening or retrying may succeed. It does not guarantee that replaying a write is safe or that the current Client remains usable. Reconcile uncertain writes after reopening.

boolean

Workers interfaces

WorkerLike

The WorkerLike interface is the minimal Worker surface accepted by a custom Client configuration.

Most applications should let create construct the packaged Worker instead.

Methods
addEventListener

The overload of this method for the message event type listens for Worker messages.

addEventListener(
  type: "message",
  listener: (event: (
    Omit<MessageEvent<any>, "data"> &
    {data: unknown}
  )) => void,
): void
TypeDescription
type"message"

The message event type.

listener(event: ( Omit<MessageEvent<any>, "data"> & {data: unknown} )) => void

The callback receiving messages from the Worker.

returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

// Count the messages the Worker sends, beside the Client's own listener.
let messages = 0;
worker.addEventListener('message', () => {
  messages++;
});

const db = await create({worker});
console.log(messages > 0);
// -> true
await db.close();

The overload of this method for the messageerror event type listens for message decoding failures.

addEventListener(
  type: "messageerror",
  listener: (event: (
    Omit<MessageEvent<any>, "data"> &
    {data: unknown}
  )) => void,
): void
TypeDescription
type"messageerror"

The messageerror event type.

listener(event: ( Omit<MessageEvent<any>, "data"> & {data: unknown} )) => void

The callback receiving message decoding failures.

returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

// Report a message from the Worker that the page could not decode.
worker.addEventListener('messageerror', () => {
  console.error('A message from the Worker could not be decoded.');
});

const db = await create({worker});
await db.close();

The overload of this method for the error event type listens for Worker runtime failures.

addEventListener(
  type: "error",
  listener: (event: ErrorEvent) => void,
): void
TypeDescription
type"error"

The error event type.

listener(event: ErrorEvent) => void

The callback receiving Worker runtime failures.

returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

// Report an error the Worker did not catch, such as a script that failed
// to load.
worker.addEventListener('error', (event) => {
  console.error('The TinyJoin Worker failed:', event.message);
});

const db = await create({worker});
await db.close();
postMessage

The postMessage method sends a request to the Worker.

postMessage(message: unknown): void
TypeDescription
messageunknown
returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

// The application's worker.ts calls startWorker() from tinyjoin/worker.
const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

// Count the requests the Client posts to its Worker.
let requests = 0;
const post = worker.postMessage.bind(worker);
worker.postMessage = (message) => {
  requests++;
  post(message);
};

const db = await create({worker});
console.log(requests > 0);
// -> true
await db.close();
removeEventListener

The overload of this method for the message event type removes a message listener.

removeEventListener(
  type: "message",
  listener: (event: (
    Omit<MessageEvent<any>, "data"> &
    {data: unknown}
  )) => void,
): void
TypeDescription
type"message"

The message event type.

listener(event: ( Omit<MessageEvent<any>, "data"> & {data: unknown} )) => void

The same callback passed to addEventListener.

returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

let messages = 0;
const countMessage = (): void => {
  messages++;
};
worker.addEventListener('message', countMessage);

const db = await create({worker});
// Count only the messages it took to open the database.
worker.removeEventListener('message', countMessage);
console.log(messages > 0);
// -> true
await db.close();

The overload of this method for the messageerror event type removes a message-error listener.

removeEventListener(
  type: "messageerror",
  listener: (event: (
    Omit<MessageEvent<any>, "data"> &
    {data: unknown}
  )) => void,
): void
TypeDescription
type"messageerror"

The messageerror event type.

listener(event: ( Omit<MessageEvent<any>, "data"> & {data: unknown} )) => void

The same callback passed to addEventListener.

returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

const reportUndecoded = (): void => {
  console.error('A message from the Worker could not be decoded.');
};
worker.addEventListener('messageerror', reportUndecoded);

const db = await create({worker});
await db.close();
// The Worker has stopped, so there is nothing left to report.
worker.removeEventListener('messageerror', reportUndecoded);

The overload of this method for the error event type removes a Worker error listener.

removeEventListener(
  type: "error",
  listener: (event: ErrorEvent) => void,
): void
TypeDescription
type"error"

The error event type.

listener(event: ErrorEvent) => void

The same callback passed to addEventListener.

returnsvoid

This has no return value.

Example
import {create} from 'tinyjoin';
import type {WorkerLike} from 'tinyjoin';

const worker: WorkerLike = new Worker(
  new URL('./worker.ts', import.meta.url),
  {type: 'module'},
);

const reportFailure = (): void => {
  console.error('The TinyJoin Worker failed.');
};
worker.addEventListener('error', reportFailure);

const db = await create({worker});
await db.close();
// The Worker has stopped, so there is nothing left to report.
worker.removeEventListener('error', reportFailure);
Properties
terminate

The terminate method stops an application-owned Worker when available.

() => void

Schema interfaces

ColumnSchema

The ColumnSchema interface describes one column of a table.

default

The default property contains the column's literal DEFAULT value, and is absent when the column declares none. A column declared DEFAULT NULL has a default of null.

JsonValue
maxLength

The maxLength property contains the most characters a VARCHAR(n) column holds, its n, and is absent for any other column.

number
name

The name property contains the column's name, as the table declares it.

string
nullable

The nullable property is false when the column is declared NOT NULL or is part of the primary key.

boolean
renamedFrom

The renamedFrom property names a column's former name, which setSchema renames the column from while the table still has a column of that name. getSchema never includes it.

string
type

The type property contains the column's runtime ColumnType.

ColumnType
ForeignKeySchema

The ForeignKeySchema interface describes one foreign key of a table: columns whose values, unless one is NULL, must be those of a row of the table it references.

columns

The columns property contains the referencing columns, in key order.

string[]
name

The name property contains the foreign key's name.

string
onDelete

The onDelete property says what deleting a referenced row does to the rows that reference it.

ForeignKeyAction
onUpdate

The onUpdate property says what changing a referenced row's referenced values does to the rows that reference it.

ForeignKeyAction
referencedColumns

The referencedColumns property contains the referenced table's columns, by position in columns: its primary key, or a unique index's columns.

string[]
references

The references property names the table the key references, which may be the table's own.

string
IndexSchema

The IndexSchema interface describes one secondary index on a table.

A primary key is not listed as an index; TableSchema's primaryKey holds it.

columns

The columns property contains the indexed column names, in index order.

string[]
name

The name property contains the index's name.

string
unique

The unique property is true for an index created with CREATE UNIQUE INDEX.

boolean
Schema

The Schema interface is returned by the getSchema method, and describes every table in the database. The setSchema method takes one to make the database's.

tables

The tables property describes the database's tables, in name order.

TableSchema[]
version

The version property contains the version the application last gave the schema with setSchema, or zero. setSchema refuses a schema whose version is lower, as an older copy of the application would give it.

number
SetSchemaOptions

The SetSchemaOptions interface configures the setSchema method.

drop

The drop property, when true, makes setSchema drop each table, and each column of a table the schema keeps, that the schema leaves out, and their data with them. Otherwise setSchema leaves them as they are.

boolean
TableSchema

The TableSchema interface describes one table, its columns, its primary key, its indexes, and its foreign keys.

columns

The columns property describes the table's columns, in declaration order, including any added later by ALTER TABLE.

ColumnSchema[]
foreignKeys

The foreignKeys property describes the table's foreign keys, in the order they were declared.

ForeignKeySchema[]
indexes

The indexes property describes the table's secondary indexes, in name order.

IndexSchema[]
name

The name property contains the table's name.

string
primaryKey

The primaryKey property contains the primary-key column names, in key order. A composite key has more than one.

string[]
renamedFrom

The renamedFrom property names a table's former name, which setSchema renames the table from while the database still has a table of that name. getSchema never includes it.

string

Functions

create

Essential

The create function opens a TinyJoin database in a dedicated Worker.

create(): Promise<Client>
returnsPromise<Client>

A Promise resolving to a Client whose Worker and database are ready.

It constructs a Client and resolves once that Client is ready, so a database that cannot be opened rejects here. The Client constructor returns before then, for code that needs the waitReady lifecycle.

The overload of this function with no arguments creates an ephemeral in-memory database using the packaged Worker. Its data is lost when the Client closes or the page reloads.

Example
import {create} from 'tinyjoin';

const db = await create();
await db.exec(`
  CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL
  )
`);
const {rows} = await db.query('SELECT * FROM tasks ORDER BY id');
await db.close();

The overload of this function with one argument, an options object, opens the database that its ClientOptions describe.

create(options: ClientOptions): Promise<Client>
TypeDescription
optionsClientOptions

Storage and Worker configuration.

returnsPromise<Client>

A Promise resolving to a Client whose Worker and database are ready.

Use the options object to select storage or provide a custom Worker. Omitting its dataDir property selects an ephemeral in-memory database.

Example
import {create} from 'tinyjoin';

const db = await create({dataDir: 'memory://'});
await db.close();

The overload of this function with a storage URL as its first argument, and optionally options as its second, opens a database at that URL.

create(
  dataDir: string | undefined,
  options?: ClientOptions,
): Promise<Client>
TypeDescription
dataDirstring | undefined

The memory:// URL, a named opfs:// URL, or undefined for the default in-memory database.

options?ClientOptions

Optional Worker configuration.

returnsPromise<Client>

A Promise resolving to a Client whose Worker and database are ready.

Use a stable opfs://name to persist data in the same browser, or memory:// for an ephemeral database. When providing the second argument, supply a storage URL and leave options.dataDir unset.

Example
import {create} from 'tinyjoin';

const db = await create('opfs://my-app');
await db.close();

Type Aliases

Query results type aliases

ChangedKeys

The ChangedKeys type maps a table name to the primary keys that changed in it.

{[table: string]: Row[]}

Each key is an object holding only that table's primary-key columns, so a composite key arrives as one object with one property per column.

RowMode

The RowMode type selects object rows or positional array rows.

"array" | "object"

An object row holds one value for each name, so a SELECT whose output names repeat is rejected unless its rows are arrays.

Data types type aliases

JsonPrimitive

The JsonPrimitive type represents a scalar value accepted by TinyJoin.

null | boolean | number | string
JsonValue

The JsonValue type represents a parameter or result value accepted by the TinyJoin JavaScript API.

JsonPrimitive | JsonValue[] | {[key: string]: JsonValue}
Row

The Row type represents the default object form of a result row.

Record<string, JsonValue>

Configuration type aliases

DataDir

The DataDir type identifies the database storage mode.

string

Use memory:// for an ephemeral database, or opfs://name for a persistent database. Calling create without a data directory also uses memory. Clients using the same OPFS name automatically share an elected database owner, including across tabs. Pending operations interrupted by owner loss reject without replay; subsequent operations reconnect automatically. Different names have independent data and do not synchronize.

Schema type aliases

ColumnType

The ColumnType type names one of the five runtime types a column holds.

"boolean" | "integer" | "float" | "text" | "json"

Each SQL type name maps to one of them: INTEGER and BIGINT to integer, REAL and DOUBLE PRECISION to float, TEXT and VARCHAR to text, and JSON and JSONB to json. The schema keeps the runtime type, not the name the table was declared with, and the length of a VARCHAR(n) as its column's maxLength.

ForeignKeyAction

The ForeignKeyAction type names what deleting or updating a referenced row does to the rows that reference it: no action and restrict refuse it, cascade deletes or updates them too, and set null and set default change their referencing columns to NULL or to the columns' defaults.

"no action" | "restrict" | "cascade" | "set null" | "set default"

node

The node module opens ephemeral TinyJoin databases in Node.js 22 or later. It constructs a Node Worker thread and loads the packaged WebAssembly engine automatically, with no extra dependencies, bundler, or polyfills.

The returned Client has the same SQL, transaction, prepared-statement, and subscription APIs as the browser Client. The module also exports ClientError and the shared Client API types, so Node applications can import their query, result, transaction, and subscription contracts from this entry point. ClientError is the same class exported by the browser entry point. See the Node guide.

Functions

create

The create function opens a fresh in-memory database in a dedicated Node Worker thread. It resolves when the database is ready.

create(dataDir?: "memory://"): Promise<Client>
TypeDescription
dataDir?"memory://"

The optional memory:// URL.

returnsPromise<Client>

A ready Client. Always await its close method when finished.

Omit the argument or pass memory://. Each call owns an independent database and Worker; close the Client to release the Worker. All data is lost when the Client closes or the process exits. OPFS, filesystem persistence, and remote synchronization are not supported by this entry point.

Example
import {type Client, create} from 'tinyjoin/node';

const db: Client = await create();
try {
  await db.exec('CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)');
  await db.query('INSERT INTO notes VALUES ($1, $2)', [1, 'Hello from Node']);
  console.log((await db.query('SELECT * FROM notes ORDER BY id')).rows);
} finally {
  await db.close();
}

References

Client

ClientError

ColumnSchema

ColumnType

ForeignKeyAction

ForeignKeySchema

IndexSchema

JsonPrimitive

JsonValue

PreparedStatement

QueryOptions

ResultField

Results

Row

RowMode

Schema

SerializedError

SetSchemaOptions

SubscriptionOptions

TableSchema

TablesChangedEvent

Transaction

worker

The worker module lets advanced applications start TinyJoin inside a Worker that they construct and bundle themselves.

Most applications should use create from the main tinyjoin module, which constructs the packaged Worker automatically.

startWorker

The startWorker function starts the TinyJoin request host in the current dedicated Worker. Persistent databases participate in automatic same-name coordination across tabs, just like the packaged Worker. Memory databases remain independent.

startWorker(): void

Example

import {startWorker} from 'tinyjoin/worker';

startWorker();

drizzle

The drizzle module connects the Drizzle ORM query builder to a TinyJoin Client. It needs drizzle-orm 0.45 or later, which the application installs itself: TinyJoin declares it as an optional peer dependency and never bundles it.

Drizzle writes PostgreSQL, so its queries run in TinyJoin's SQL dialect, and its schemas use the column types that dialect has. See the Drizzle guide for what works, and what does not.

Interfaces

Migrations interfaces

MigrationConfig

The MigrationConfig interface describes the migrations that the migrate function applies.

journal

The journal property is the Drizzle Kit journal, which an application can import from meta/_journal.json.

MigrationJournal
migrations

The migrations property maps each migration's file to its SQL text. A key may be the migration's tag, its file name, or any path ending in its file name, so the object that a bundler's glob import returns can be passed as it is.

Record<string, string>
migrationsTable

The migrationsTable property names the table that records the migrations applied, __drizzle_migrations unless it is given.

string
MigrationJournal

The MigrationJournal interface is the journal that drizzle-kit generate writes beside its migrations, as meta/_journal.json, of which the migrate function reads each entry's tag and time.

entries

The entries property lists the migrations in the order to apply them, each with its tag, which names its SQL file, and the time it was generated.

{
  tag: string;
  when: number;
}[]

Push interfaces

PushOptions

The PushOptions interface configures the push function.

drop

The drop property, when true, drops each table, and each column of a table the schema keeps, that the schema leaves out, with their data. Otherwise they stay as they are.

boolean
renames

The renames property maps the new name of each renamed table, as table, or column, as table.column, to the name it had, so that push renames it, keeping its data, rather than creating it empty. A rename that has already happened changes nothing.

Record<string, string>
version

The version property gives the schema a version, zero unless it is given, which the database records. A schema whose version is lower than the database's is refused with SCHEMA_OUTDATED, so that a tab running an older copy of the application cannot undo a newer one's changes.

number

Functions

Drizzle functions

drizzle

The drizzle function creates a Drizzle database over a TinyJoin Client.

drizzle<TSchema>(
  client: Client,
  config?: DrizzleConfig<TSchema>,
): TinyJoinDatabase<TSchema>
TypeDescription
clientClient

The TinyJoin Client to query, from create().

config?DrizzleConfig<TSchema>

Drizzle's configuration: its schema, for relational queries, and its logger, casing, and cache.

returnsTinyJoinDatabase<TSchema>

A Drizzle database.

Queries run on the Client as array rows, which Drizzle maps to its own result shapes. Drizzle's db.transaction runs on the Client's callback transaction, so it holds the database as any TinyJoin transaction does, tx.rollback discards its work, and a transaction cannot nest, since there are no savepoints, or take an isolation level.

A value Drizzle encodes for a json or jsonb column is bound as the JSON value it is, rather than as the JSON text Drizzle makes of it, which TinyJoin would store as a string.

Close the Client yourself when the database is no longer needed.

Example
import {create} from 'tinyjoin';
import {drizzle} from 'tinyjoin/drizzle';
import {eq} from 'drizzle-orm';
import {boolean, pgTable, text} from 'drizzle-orm/pg-core';

const tasks = pgTable('tasks', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  done: boolean('done').notNull().default(false),
});

const client = await create('opfs://my-app-v1');
const db = drizzle(client, {schema: {tasks}});
await db.insert(tasks).values({id: crypto.randomUUID(), title: 'Try Drizzle'});
const open = await db.select().from(tasks).where(eq(tasks.done, false));

Migrations functions

migrate

The migrate function applies the migrations that drizzle-kit generate wrote, which the database has not yet recorded, in the journal's order.

migrate<TSchema>(
  db: TinyJoinDatabase<TSchema>,
  config: MigrationConfig,
): Promise<void>
TypeDescription
dbTinyJoinDatabase<TSchema>

The Drizzle database, from drizzle.

configMigrationConfig

The journal and the SQL text of each migration.

returnsPromise<void>

This has no return value.

Each migration runs as one script together with the row that records it, so it commits whole or not at all, and a migration that fails leaves the database as the previous one left it, and rejects with its error. A migration another Client of the same database applied first is skipped, so every tab of an application can call migrate as it starts.

A migration is recorded by its tag and applied once: a file changed after it was applied is not applied again, and a migration that joins the journal before others already applied is still applied. The migrations need no file system, so a browser application bundles them.

Example
import {create} from 'tinyjoin';
import {drizzle, migrate} from 'tinyjoin/drizzle';
import journal from './drizzle/meta/_journal.json';

const db = drizzle(await create('opfs://my-app-v1'));
await migrate(db, {
  journal,
  migrations: import.meta.glob<string>('./drizzle/*.sql', {
    query: '?raw',
    import: 'default',
    eager: true,
  }),
});

Push functions

push

The push function makes the database hold the tables of a Drizzle schema, as Drizzle Kit's push would for PostgreSQL, but from the application and as one change that commits whole or not at all.

push<TSchema>(
  db: TinyJoinDatabase<TSchema>,
  schema: Record<string, unknown>,
  options?: PushOptions,
): Promise<boolean>
TypeDescription
dbTinyJoinDatabase<TSchema>

The Drizzle database, from drizzle.

schemaRecord<string, unknown>

The Drizzle schema, whose tables push reads, leaving out its relations.

options?PushOptions

The schema's version, whether to drop what it leaves out, and what it renames.

returnsPromise<boolean>

A Promise resolving to true if the database changed, or false if it already held the schema.

It reads each table's columns, primary key, unique constraints, and indexes from Drizzle, and passes them to the Client's setSchema, which creates what the database lacks and changes what differs. A schema the database already holds changes nothing, so every tab of an application can call push as it starts, in place of migrations.

A table that declares what TinyJoin does not have, such as a foreign key, a timestamp column, an SQL default, or an index on an expression, is refused before anything changes. A default Drizzle works out in JavaScript, with $defaultFn, is Drizzle's, not the database's.

Example
import {create} from 'tinyjoin';
import {drizzle, push} from 'tinyjoin/drizzle';
import * as schema from './schema';

const db = drizzle(await create('opfs://my-app-v1'), {schema});
await push(db, schema, {version: 2, renames: {'tasks.name': 'title'}});

Type Aliases

TinyJoinDatabase

The TinyJoinDatabase type is the Drizzle database the drizzle function returns: Drizzle's PostgreSQL database with the TinyJoin Client it queries as its $client.

(
  PgRemoteDatabase<TSchema> &
  {$client: Client}
)
TypeDescription
$clientClient

Drizzle's raw db.execute resolves to the rows its statement returns, as objects.

kysely

The kysely module connects the Kysely query builder to a TinyJoin Client. It needs kysely 0.28 or later, which the application installs itself: TinyJoin declares it as an optional peer dependency and never bundles it.

Kysely writes PostgreSQL, so its queries run in TinyJoin's SQL dialect. See the Kysely guide for what works, and what does not.

Classes

TinyJoinDialect

The TinyJoinDialect class is a Kysely dialect that runs Kysely's PostgreSQL queries on a TinyJoin Client.

Kysely runs one query at a time on the Client, so a query outside a transaction waits until the transaction ends rather than failing. Kysely's db.transaction runs on the Client's callback transaction, which holds the database, and every tab sharing it, until it commits or rolls back. It takes no isolation level or access mode, and there are no savepoints.

Its introspector reads the Client's getSchema, and Kysely's Migrator runs each migration's statements in turn, since TinyJoin runs DDL outside transactions, with a Web Lock keeping one tab migrating at a time.

Example
import {create} from 'tinyjoin';
import {TinyJoinDialect} from 'tinyjoin/kysely';
import {Kysely} from 'kysely';

interface Database {
  tasks: {id: string; title: string; done: boolean};
}

const client = await create('opfs://my-app-v1');
const db = new Kysely<Database>({dialect: new TinyJoinDialect({client})});
const open = await db
  .selectFrom('tasks')
  .select(['id', 'title'])
  .where('done', '=', false)
  .execute();
Constructors
constructor

The constructor creates a dialect over a TinyJoin Client.

new TinyJoinDialect(config: TinyJoinDialectConfig): TinyJoinDialect
TypeDescription
configTinyJoinDialectConfig

The dialect's configuration, with its Client.

returnsTinyJoinDialect

Example
import {create} from 'tinyjoin';
import {TinyJoinDialect} from 'tinyjoin/kysely';
import {Kysely} from 'kysely';

interface Database {
  tasks: {id: string; title: string};
}

const client = await create();
const dialect = new TinyJoinDialect({client});
const db = new Kysely<Database>({dialect});
await db.schema
  .createTable('tasks')
  .addColumn('id', 'text', (column) => column.primaryKey())
  .addColumn('title', 'text', (column) => column.notNull())
  .execute();

// Kysely's destroy leaves the Client open, so the application closes it.
await db.destroy();
await client.close();
Methods
createAdapter

The createAdapter method creates the adapter that tells Kysely how TinyJoin differs from PostgreSQL. Kysely calls it.

createAdapter(): DialectAdapter
returnsDialectAdapter

A Kysely dialect adapter.

Example
import {create} from 'tinyjoin';
import {TinyJoinDialect} from 'tinyjoin/kysely';

const client = await create();
const adapter = new TinyJoinDialect({client}).createAdapter();

// TinyJoin runs DDL outside transactions, so Kysely's Migrator must too.
console.log(adapter.supportsTransactionalDdl);
// -> false
console.log(adapter.supportsReturning);
// -> true
await client.close();
createDriver

The createDriver method creates the driver that runs Kysely's queries on the Client. Kysely calls it.

createDriver(): Driver
returnsDriver

A Kysely driver.

Example
import {create} from 'tinyjoin';
import {TinyJoinDialect} from 'tinyjoin/kysely';
import {Kysely} from 'kysely';

interface Database {
  tasks: {id: string; title: string};
}

const client = await create();
await client.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const db = new Kysely<Database>({dialect: new TinyJoinDialect({client})});

// Kysely creates the driver for its first query, and runs each query on
// the Client.
await db
  .insertInto('tasks')
  .values({id: 'a', title: 'Write docs'})
  .execute();
const {rows} = await client.query('SELECT * FROM tasks');
console.log(rows);
// -> [{id: 'a', title: 'Write docs'}]
await client.close();
createIntrospector

The createIntrospector method creates the introspector that lists the database's tables and columns from the Client's getSchema. Kysely calls it.

createIntrospector(): DatabaseIntrospector
returnsDatabaseIntrospector

A Kysely database introspector.

Example
import {create} from 'tinyjoin';
import {TinyJoinDialect} from 'tinyjoin/kysely';
import {Kysely} from 'kysely';

const client = await create();
await client.exec(`
  CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT NOT NULL)
`);
const db = new Kysely<unknown>({dialect: new TinyJoinDialect({client})});

// Kysely reads the tables through the introspector.
const [tasks] = await db.introspection.getTables();
console.log(tasks.name);
// -> 'tasks'
console.log(tasks.columns.map(({name, dataType}) => [name, dataType]));
// -> [['id', 'text'], ['title', 'text']]
await client.close();
createQueryCompiler

The createQueryCompiler method creates Kysely's PostgreSQL query compiler. Kysely calls it.

createQueryCompiler(): QueryCompiler
returnsQueryCompiler

A Kysely query compiler.

Example
import {create} from 'tinyjoin';
import {TinyJoinDialect} from 'tinyjoin/kysely';
import {Kysely} from 'kysely';

interface Database {
  tasks: {id: string; title: string};
}

const client = await create();
const db = new Kysely<Database>({dialect: new TinyJoinDialect({client})});

// Kysely compiles each query with the compiler, into PostgreSQL's SQL.
const {sql, parameters} = db
  .selectFrom('tasks')
  .select('title')
  .where('id', '=', 'a')
  .compile();
console.log(sql);
// -> 'select "title" from "tasks" where "id" = $1'
console.log(parameters);
// -> ['a']
await client.close();

Interfaces

TinyJoinDialectConfig

The TinyJoinDialectConfig interface configures a TinyJoinDialect.

client

The client property is the TinyJoin Client, from create(), on which Kysely runs its queries. The application closes it: Kysely's destroy leaves it open.

Client

vite

The vite module provides optional production offline caching for Vite applications, including TinyJoin's lazily loaded Worker, OPFS, and WASM files. This Node-only build integration is separate from the browser runtime.

Interfaces

TinyjoinOfflineOptions

The TinyjoinOfflineOptions interface configures production offline caching.

mode

The mode property selects automatic service-worker registration or integration with an existing service worker. The default is service-worker.

"service-worker" | "manifest"

The manifest mode emits tinyjoin-precache.json and tinyjoin-precache.js without registering or replacing a service worker.

navigationFallback

The navigationFallback property names the emitted HTML file served for otherwise unmatched navigation requests within the application scope. It defaults to index.html; use false to disable SPA navigation fallback.

string | false

Functions

tinyjoinOffline

The tinyjoinOffline function returns a Vite plugin that precaches the complete emitted production build, including lazy runtime assets, with verified content hashes. It does not register a service worker during development.

tinyjoinOffline(options?: TinyjoinOfflineOptions): Plugin
TypeDescription
options?TinyjoinOfflineOptions

Optional offline-caching settings.

returnsPlugin

A Vite plugin that configures production offline caching.

The first visit requires a network connection. Wait for navigator.serviceWorker.ready before expecting a subsequent navigation to work offline. Storage persistence and application-file caching remain separate: OPFS stores the database, while the service worker stores the application.

Updates wait until all tabs controlled by the old service worker close. The plugin does not force a new worker onto running tabs. Serve the whole build together at one same-origin root-relative or relative Vite base. External requests and files created after the build are not cached.

Example
import {defineConfig} from 'vite';
import {tinyjoinOffline} from 'tinyjoin/vite';

export default defineConfig({plugins: [tinyjoinOffline()]});