npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@boxdb/client

v0.5.1

Published

BoxDB queries and SQLite driver backed by the Rust core

Readme

BoxDB for TypeScript

An asynchronous Node.js and Bun SQLite client backed by the BoxDB Rust core. Each connection owns a native worker: SQL execution and lock waits leave the JavaScript event loop free. Binary SQLite files, readable YAML storage, extended column types and built-in SQL extensions use the same core.

Install

npm install @boxdb/client

Requires Node.js 22.18+ or Bun on macOS ARM64 (macOS 11+) or Linux x64 or ARM64 with glibc 2.35+ (Ubuntu 22.04+ or 64-bit Raspberry Pi OS Bookworm+). All three native addons are bundled in the npm package; installation needs no Rust compiler, install scripts or extra downloads. Alpine/musl, Windows, Intel macOS and 32-bit ARM are not supported by this npm release. This client does not run in browsers.

Build from source

Requires Node.js 22.18+, Bun, Rust, native build tools and libclang; see the repository README. From the repository root:

bun install --frozen-lockfile
bun run build
bun run check
bun run test

just ts-build and just ts-check ts-test also build and verify all packages. For an optimized addon, run bun run --cwd packages/typescript build:release, then build the adapter packages. Builds target the current OS and architecture; the npm release builds are described in the release guide.

Queries

Query rows are unknown[]. Validate them before use with a schema library such as Zod:

npm install zod
import { BoxDB } from "@boxdb/client";
import { z } from "zod";

const User = z.object({ id: z.bigint(), name: z.string() });

await using db = await BoxDB.open("data.db", { busyTimeoutMs: 5000 });
await db.exec("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, name TEXT)");
const inserted = await db.query`INSERT INTO users(name) VALUES(${"Ada"})`.run();

const rows = User.array().parse(
  await db.query`
  SELECT * FROM ${db.identifier("users")}
  ${db.fragment`WHERE id = ${inserted.lastInsertRowid}`}
  LIMIT ${100}
  `,
); // { id: bigint; name: string }[] after runtime validation

Scalar interpolations become bound parameters; do not put SQL quotes around them. identifier("main", "users") quotes separate components; identifier("user.name") quotes one name containing a dot. Fragments snapshot parameters and compose without executing SQL.

Queries execute once, lazily. await query and query.execute({ signal }) return row objects as unknown[]. query.get({ signal }) returns the first row or undefined, typed as unknown. query.values({ signal }) returns positional rows as unknown[], preserving duplicate column names. Validate each result against the shape selected by the query. query.run({ signal }) returns { changes, lastInsertRowid } and requires a statement without returned columns. These methods share the execution result or error; the first execution chooses its signal. query.compile() returns a copy of { sql, parameters } without executing.

For SQL from an adapter or migration tool:

const result = await db.execute("SELECT name FROM users WHERE id=:id", { id: 1 });
// { rows, columns, changes, lastInsertRowid }
const names = User.pick({ name: true }).array().parse(result.rows);
const positional = await db.execute("SELECT 1 AS x, 2 AS x", [], { raw: true });
await db.exec("CREATE INDEX IF NOT EXISTS users_name ON users(name);");

execute accepts one statement and positional or named bindings and returns rows: unknown[] in both object and raw modes. exec accepts scripts without parameters. Queries accept no result type parameter; derive application types from validated data. The client has no dependency on a validation library. Explicit prepared statements and streaming are not part of this API.

Transactions and cancellation

Use TypeScript 5.2+; transpile await using for runtimes without native syntax support (including Node.js 22).

await using tx = await db.begin({ mode: "immediate", signal });
await tx.query`INSERT INTO users(name) VALUES(${"Grace"})`;
{
  await using nested = await tx.begin();
  await nested.query`INSERT INTO users(name) VALUES(${"temporary"})`;
  // Exiting without commit rolls back this savepoint.
}
await tx.commit();

Commit is explicit. Disposal and rollback() discard unfinished work. Root transactions use immediate by default, acquiring the writer before transaction work starts. Explicit deferred and exclusive modes remain available.

Use db.begin({ readOnly: true }) for a deferred transaction that rejects SQL writes with BOXDB_READONLY. In binary WAL databases it can read a consistent snapshot while another connection writes. The native guard covers queries and scripts, including attempts to disable query_only; the previous connection setting is restored after the transaction. Read-only connections automatically start read-only transactions. Combining read-only with an immediate or exclusive mode throws TypeError.

await using tx = await db.begin({ readOnly: true });
const total = z.object({ count: z.bigint() }).parse(
  await tx.query`SELECT count(*) AS count FROM users`.get(),
);
await tx.commit();

Nested transactions use savepoints and inherit the outer read-only restriction and deadline. They have no independent mode. Use the innermost transaction handle for work inside a transaction; parent-handle SQL fails while a nested handle owns the connection. Root requests, including db.begin(), wait for the entire transaction to finish. Root SQL then executes independently in autocommit, in submission order. SQL transaction-control statements must use handles too.

Transactions have a five-second lifetime by default, starting after BEGIN succeeds. Set transactionTimeoutMs on the client or override it with timeoutMs on db.begin(); both accept integers from 1 to 2147483647 milliseconds. Savepoints inherit the outer deadline and do not extend it. Expiry interrupts running SQL, cancels queued transaction work and rolls back. Waiting root requests resume only after cleanup; further queries or commits through the expired handle fail with BOXDB_TRANSACTION_TIMEOUT.

await using db = await BoxDB.open(":memory:", { transactionTimeoutMs: 10_000 });
await using tx = await db.begin({ timeoutMs: 2_000 });
const pending = db.query`SELECT 1 AS answer`.execute();
await tx.commit();
await pending; // Executes outside the completed transaction.

Awaiting a root request before finishing your own transaction waits until its timeout rolls it back. Use tx for operations needed before commit or rollback. The deadline starts cancellation; freeing the connection still requires native SQL interruption and rollback to complete.

tx.active reports whether its transaction still exists. A failed commit leaves an active SQLite transaction available for repair or retry. If SQLite itself rolls back a transaction after an interrupt, further work through that handle fails instead of escaping into autocommit.

const controller = new AbortController();
const pending = db.query`SELECT * FROM users`.execute({ signal: controller.signal });
controller.abort(new Error("request no longer needed"));
await pending; // Rejects with the cancellation reason if cancellation wins.

Signals cancel queued requests before execution and interrupt running SQL or lock waits. A transaction inherits its signal; aborting an idle transaction also rolls it back. Rollback cleanup cannot be cancelled by that signal. Cancellation cannot undo a statement that has already committed; use a transaction for atomic multi-statement work.

close() rejects new root requests, drains accepted work and waits for active transactions to finish. Their handles can still commit or roll back. close({ mode: "abort" }) cancels pending and running work, rolls back active transactions and waits for cleanup. An existing draining close can be escalated to abort. await using db performs a draining close.

Options, values and diagnostics

BoxDB.open(path, options) accepts format: "binary" | "readable", readOnly, busyTimeoutMs, transactionTimeoutMs, maxPendingQueries, and signal. Existing contents determine the format. A read-only readable connection reads an opening snapshot without taking the writer's lock.

Inputs accept null, strings, finite numbers, bigint, boolean and Uint8Array. Bigint parameters accept the combined INT64/UINT64 range (-2^63 through 2^64 - 1); positive values above INT64_MAX bind as UINT64. Use CAST(? AS UINT8/16/32) to select a narrower unsigned width. Safe integer numbers and booleans bind as integers; fractional numbers bind as REAL. Serialize objects explicitly. Every signed or unsigned integer cell, write count and row ID returns as bigint; REAL remains number. Convert bigint explicitly when serializing JSON. Unsafe integer inputs throw instead of rounding.

Results retain native decimal values as exact text. DECIMAL arithmetic, comparisons and ordering run in the engine; NUMERIC is not supported. UUID and temporal values return canonical strings: INSTANT uses UTC, TIME has no date or time zone, DATETIME has no time zone, and temporal strings preserve microsecond precision. JSON and arrays remain opaque strings; SQL JSON processing is disabled. Blobs are Buffer values. See the native decimal guide and temporal types.

BoxDBError preserves code, resultCode, extendedCode and cause. Engine failures have BOXDB_* names, such as BOXDB_CONSTRAINT_UNIQUE. Validation, closed connections and native infrastructure failures have separate categories; an exhausted queue uses BOXDB_QUEUE_FULL. findBoxDBError(error) follows causes and aggregate members without looping on cycles. Invalid arguments throw TypeError or RangeError. See the client error contract.

The optional queue limit bounds waiting operations, including root requests waiting for a transaction; cleanup always gets through. db.diagnostics reports pending/active/completed operations and the last queue wait and execution durations in milliseconds. A duration covers a complete scheduled operation, including preparation and native execution.

File diagnostics and recovery

For existing files, use await BoxDB.checkFile(path, "full"), await BoxDB.repairFile(source, destination) or await BoxDB.recoverFile(source, destination, "binary") independently of a live client. Reports distinguish invalid data from incomplete checks. Recovery can lose data even when its output passes validation; see diagnostics and recovery.

SQL migrations

import { MigrationSet } from "@boxdb/client";

const migrations = await MigrationSet.read("db/migrations");
await migrations.up(db);

Use MigrationSet.fromFiles([{ filename, sql }]) to bundle SQL in the application. The same set exposes status(db) and down(db); execution methods accept { signal }. These methods use the CLI's format and history, execute on the native worker and wait for active transactions on the root client. See migration sources, cancellation and rollback.

Integrations

Adapters are independent npm packages with their libraries as peer dependencies. They borrow the client and do not close it:

  • Drizzle adapter: asynchronous queries, transactions, batches and migrations.
  • Better Auth adapter: direct database adapter, schema generation and native atomic credential/counter operations.
  • db0 adapter: a SQLite connector accepting a database or transaction handle.

Every connection includes decimal arithmetic, Unicode text functions, regular expressions, sqlite-vec, FTS5 and R*Tree; see the SQL extension guide. Virtual tables require memory or binary storage.