This page covers the CLI, runtime API, HTTP client, and package exports.
With a package script like "db": "async-db":
npm run db -- sync
npm run db -- types
npm run db -- types --watch
npm run db -- types --out ./src/generated/db.types.d.ts
npm run db -- schema
npm run db -- schema users
npm run db -- schema infer users
npm run db -- schema infer users --out db/users.schema.jsonc
npm run db -- schema manifest --out ./src/generated/db.schema.json
npm run db -- schema validate
npm run db -- viewer manifest --out ./src/generated/db.viewer.json
npm run db -- operations build
npm run db -- operations build --out ./src/generated/db.operations.json --refs-out ./src/generated/db.operation-refs.json
npm run db -- contracts check
npm run db -- contracts refs --out ./src/generated/db.contract-refs.json
npm run db -- usage scan ./src --production
npm run db -- usage scan ./src --production --out ./src/generated/db.usage.json
npm run db -- doctor
npm run db -- doctor --production
npm run db -- doctor --production --usage ./src --json
npm run db -- doctor --json
npm run db -- check --strict --production
npm run db -- create users '{"id":"u_2","name":"Grace Hopper","email":"grace@example.com"}'
npm run db -- serve
npm run db -- generate hono
npm run db -- generate hono --api rest,graphql --out ./serverInside npm scripts, db resolves to the local dependency binary. Equivalent direct commands:
async-db sync
async-db types
async-db schema validate
async-db viewer manifest --out ./src/generated/db.viewer.json
async-db operations build
async-db usage scan ./src --production
async-db doctor
async-db doctor --production
async-db doctor --production --usage ./src --json
async-db check --strict --production
async-db serve
async-db generate honoWith pnpm and a "db": "async-db" script, pass arguments directly to the script name:
pnpm db sync
pnpm db schema validate
pnpm db serveimport { openDb } from '@async/db';
const db = await openDb({
outputs: {
stateDir: './.db',
},
stores: {
default: 'json',
},
});
const users = db.collection('users');
await users.create({
id: 'u_2',
name: 'Grace Hopper',
email: 'grace@example.com',
role: 'user',
});
const ada = await users.get('u_1');
const hasGrace = await users.exists('u_2');
await db.close();Call db.close() when a long-running process is done with the database so stores with open handles, such as SQLite, can release them.
Use createDbRuntime() when custom Node middleware should own the same
development lifecycle as async-db serve: open the db, sync or hydrate, watch
fixture sources, publish lifecycle events, expose request middleware, and clean
everything up together.
import http from 'node:http';
import { createDbRuntime } from '@async/db';
const runtime = await createDbRuntime({
cwd: process.cwd(),
watch: true,
});
const server = http.createServer((request, response) => {
runtime.handleRequest(request, response).then((handled) => {
if (!handled) {
response.writeHead(404).end();
}
});
});
server.once('close', () => {
void runtime.close();
});Use createDbRequestHandler(db, options) only when app code already owns the
database lifecycle and file watching.
Singleton document usage:
const settings = db.document('settings');
await settings.set('/theme', 'dark');
await settings.set(['ui', 'sidebar', 'collapsed'], true);
await settings.set('locale', 'en-US');
const value = await settings.get('/theme');
const collapsed = await settings.get(['ui', 'sidebar', 'collapsed']);Document paths support JSON Pointer strings such as /ui/theme, exact array
paths such as ['ui', 'theme'], and bare top-level string shorthand such as
'theme'. Use document.put(value) to replace the whole document.
Fork and branch usage:
const tenant = await db.forks.ensure('tenant_acme', {
from: 'main',
metadata: {
purpose: 'tenant',
plan: 'free',
},
});
const snapshot = await tenant.snapshots.create({
label: 'before-projects-migration',
resources: ['projects'],
});
await tenant.migrations.start('projects-to-postgres', {
resources: ['projects'],
mode: 'read-only',
});
await tenant.resources.migrate('projects', {
from: 'json',
to: 'postgres',
});
await tenant.migrations.verify('projects-to-postgres', {
resources: ['projects'],
checks: ['count', 'checksum'],
});
await tenant.routing.set({
projects: 'postgres',
});
await tenant.migrations.finish('projects-to-postgres');
void snapshot;These are low-level database lifecycle APIs. App code decides whether a fork is a tenant, preview, debug copy, demo, or test environment.
Import generated DbTypes from .db/types/index.d.ts or from a committed output file when typed collection names and records should be available to TypeScript. Apps can also add a TypeScript paths alias such as #db/types for the committed generated file; see Generated Types.
Use loadDbSchema({ from }) when app code needs the schema contract without
opening runtime stores or reading source records. from can point at a project
root, a db/ folder, the root db.schema.mjs / db.schema.js, or one resource schema file.
import { loadDbSchema, openDb } from '@async/db';
const schema = await loadDbSchema({ from: './db.schema.mjs' });
const validateUserInput = schema.validator('users', {
mode: 'create',
unknownFields: 'strip',
});
const input = validateUserInput.assert(await request.json());Validators reject computed and read-only fields. They default unknown fields to
error; use strip, allow, or warn when an endpoint has a different input
contract. mode: 'patch' allows partial records and mode: 'replace' keeps
required-field checks strict.
Call computed field resolvers directly when server code wants the same field logic that REST and GraphQL use:
const userResolvers = schema.resolver('users', {
value: input,
context: {
locale: 'en-US',
nameFormatter,
},
});
const fullName = await userResolvers.fullName();schema.resolver('users.fullName') returns one callable resolver. The resolver
this value is a delegated context with this.get(name) and this.has(name).
User context values win over internal values; this._internal exposes the
unoverridden internal view when a resolver needs it. A resolver call can also
pass ad hoc arguments, such as { record: input }, when the schema function is
written to receive them.
Pass a loaded schema to openDb({ schema }) when one process wants to inspect
or validate the contract first, then open the runtime database from the same
schema locator:
const schema = await loadDbSchema({ from: './db.schema.mjs' });
const db = await openDb({ schema });loadDbSchema() is metadata-only by default and does not call content/data
readers, runtime stores, or computed resolvers. openDb() defaults to runtime
loading and reads the matching fixture/content sources.
JavaScript schema files can describe folder content sources with the helper
exported from @async/db/schema:
import { collection, field, files } from '@async/db/schema';
export default collection({
source: files('./**/*.mdx', { read: 'frontmatter' }),
fields: {
id: field.string({ required: true }),
title: field.string({ required: true }),
body: field.string(),
},
});Keep runtime store selection in db.config.mjs, for example
resources.docs.store = 'static'.
import { createDbClient } from '@async/db/client';
const client = createDbClient({
baseUrl: 'http://127.0.0.1:7331',
batching: true,
});
const users = await client.rest.get('/db/users.json');
await client.rest.post('/db/users', {
id: 'u_2',
name: 'Grace Hopper',
email: 'grace@example.com',
});
const batch = await client.rest.batch([
{ method: 'GET', path: '/db/users.json' },
{ method: 'GET', path: '/db/settings.json' },
]);When using createDbClient() directly against standalone async-db serve, use
the app-facing /db routes. Scoped clients, such as the Vite virtual client or
a fork client, can keep resource paths like /users because the client sets a
restBasePath for you.
The client can batch requests made within a short timeout. The default batching window is 10ms. Identical REST GET requests are deduped by default. Writes are not deduped unless you explicitly choose dedupe: 'all'.
Enable the browser cache explicitly when app code should reuse normalized REST and GraphQL reads:
import { createDbClient, createIndexedDbCacheStorage } from '@async/db/client';
const client = createDbClient({
baseUrl: 'http://127.0.0.1:7331',
cache: {
enabled: true,
storage: 'memory',
readPolicy: 'cache-first',
writePolicy: 'merge-and-invalidate',
eventPolicy: 'invalidate',
},
});
await client.rest.get('/db/users.json?select=id,name', { cache: 'cache-first' });
await client.graphql('{ users { id name __typename } }', {}, { cache: 'cache-and-network' });
const stop = client.cache.watch(
{ kind: 'rest', method: 'GET', path: '/db/users.json?select=id,name' },
(snapshot) => {
render(snapshot.data);
},
);
const persistedClient = createDbClient({
baseUrl: 'http://127.0.0.1:7331',
cache: {
enabled: true,
storage: createIndexedDbCacheStorage({ name: 'async-db' }),
},
});The cache is off by default. When enabled, the client fetches the viewer
manifest once, normalizes collection records by resource id, normalizes
documents by resource name, and keeps query results by canonical request key.
Cacheable reads use exact in-flight dedupe outside the batching window. Runtime
write events from /__db/log invalidate or refetch affected resources according
to eventPolicy; fixture/source reload events from /__db/events refresh the
manifest and invalidate cached queries. IndexedDB is explicit opt-in because it
persists record data in the browser.
Run registered queries or literal operation templates through the same client.
query() is the app-facing alias for operation():
import operationRefs from './generated/db.operation-refs.json' assert { type: 'json' };
await client.query('GetUser', { id: 'u_1' });
await client.query('/db/users/{id}.json?select=id,name', { id: 'u_1' });
await client.query({
method: 'GET',
path: '/db/users/{id}.json',
query: {
select: 'id,name',
},
}, { id: 'u_1' });
await client.query({
query: 'query GetUser($id: ID!) { user(id: $id) { id name } }',
operationName: 'GetUser',
variables: {
id: '{id}',
},
}, { id: 'u_1' });
await client.query({ name: 'GetUser', ref: 'users.get' }, { id: 'u_1' });
await client.query(operationRefs.operations.GetUser.ref, { id: 'u_1' });Package/runtime callers can also pass contract context for enforcement:
await db.query(operationRefs.operations.GetUser.ref, {
id: 'u_1',
}, {
contract: 'public',
});String values passed to query() that start with /, or with an HTTP method
followed by /, are literal REST templates. Other strings are registered query
refs, such as an operation name or explicit ref, and call POST /__db/operations/:ref. Object REST templates execute as normal REST requests.
Object GraphQL templates are inferred when an object has query and no REST
path, and execute as normal GraphQL requests. The server looks up registered
refs, substitutes variables, and runs REST templates through normal REST shaping
or GraphQL templates through the GraphQL executor.
Generated operation refs include .name and .ref. .ref is the value app
code should call. It defaults to hashOperation(template) unless the operation
source provides an explicit ref. Server acceptance is controlled separately
with operations.acceptRefs.
| Export | Use |
|---|---|
@async/db |
Runtime API such as openDb. |
@async/db/schema |
.schema.mjs and .schema.js authoring helpers. |
@async/db/config |
defineConfig and manifest helpers. |
@async/db/client |
HTTP client with REST, GraphQL, and batching helpers. |
@async/db/json |
First-party JSON file database capabilities and safe JSON state helpers. |
@async/db/vite |
Optional Vite dev server plugin. |
@async/db/hono |
Optional Hono route registration helpers. |
@async/db/sqlite |
Optional SQLite adapter helpers. |
@async/db/postgres |
Optional Postgres runtime store helpers using an injected client. |
@async/db/kv |
Optional generic KV runtime store helpers using an injected get/set client. |
@async/db/redis |
Optional Redis-named helper over the generic KV store. |
The core package stays dependency-light. Optional integrations use dynamic imports, generated app dependencies, or injected database clients.
@async/db/json is the first-party JSON file database subpath. It exposes the
JSON store capability metadata and safe file-state helpers for tooling,
diagnostics, exports, and migrations. Most app code should still use openDb(),
createDbClient(), and registered operations so resources can graduate from
JSON to SQLite, Postgres, or custom stores without changing client calls.
The root export also includes hashOperation(), buildOperationManifest(),
and createDbOperationHandler() for tools and framework adapters that want to
build or execute registered operation registries without shelling out to the
CLI.
createDbOperationHandler(db, options?) returns a small operation executor:
const handler = createDbOperationHandler(db, {
registry: generatedOperations.operations,
acceptRefs: 'ref',
});
const result = await handler.execute(operationRefs.operations.GetUser.ref, {
id: 'u_1',
}, {
contract: 'public',
});Use execute(ref, variables, options) for direct calls or
executeRequest(ref, body, options) when adapting an HTTP request body shaped
as { variables, contract }. Framework adapters should pass registry,
acceptRefs, resolveRef, or validateRef at handler creation time, and pass
the request contract at execution time.
Use buildContractRefsManifest(), checkContracts(),
inferContractsFromTags(), and inferContractsFromUsage() for build-tool
workflows that need the same contract logic without shelling out to the CLI.
Inline registries can use full operation objects or string REST templates. The registry key is used as the fallback name and ref, so custom build steps can keep a small manual registry:
const handler = createDbOperationHandler(db, {
registry: {
GetUser: '/users/{id}.json?select=id,name',
},
acceptRefs: 'name',
});Run every repo example and open an index of their viewers:
npm run examplesThe examples index runs on one loopback port and starts each example runtime lazily when you open its demo or /__db viewer.
To get an HTTPS URL for the examples index inside your tailnet, opt in to Tailscale Serve:
npm run examples -- --tailscale-serveThis runs tailscale serve --bg <port> after the local examples host starts.
@async/db does not call tailscale cert, manage local certificate files, or
change tailnet settings directly. If MagicDNS or HTTPS certificates still need
admin setup, the Tailscale CLI output is shown so you can follow its prompt.