Configuration
Most projects can skip db.config.js at first. Add config when defaults stop matching the project.
Use defineConfig for editor autocomplete and inline type checks:
// @ts-check
import { defineConfig } from '@async/db/config';
export default defineConfig({
stores: {
default: 'json',
},
mock: {
delay: [30, 100],
},
});
See db.config.example.js for a commented config with common values.
Config Map
| Need | Default | Configure |
|---|---|---|
Data folder (db/) | ./db | dbDir |
| Custom source formats | Built-in readers | sources.readers |
| Git content remotes | Off | git.remotes and @async/db/git |
| Git-backed resource mirror | JSON under .db/state | git.mirror |
| Nested resource names | Data file basename | resources.naming or resources.customizeResource |
| Runtime store behavior | JSON files under .db/state | stores.default or resources.<name>.store |
| Index intent metadata | Off | resources.<name>.indexes |
| Generated output paths | .db, .db/types/index.d.ts | outputs |
| Importable generated types | Off | outputs.committedTypes |
| Importable schema manifest | Off | outputs.schemaManifest |
| Importable viewer manifest | Off | outputs.viewerManifest |
| Named config policy bundle | None | profile and profiles |
| Runtime non-secret config values | None | env.var(...) |
| Runtime secret config values | None | env.secret(...) |
| Registered query operation refs | Off | operations |
| Contract-scoped operation refs | Off | outputs.contractRefs and contracts |
| REST response formats | .json, .html, .md | rest.formats |
| App-facing data route base | /db | server.dataPath |
| Route exposure policy | Open | server.expose |
| Unknown fields | Warn | schema.unknownFields |
| Standard Schema-first output | Off | schema.standardSchema |
| Schema defaults | Create and safe hydration | defaults |
| Schema-only mock records | Off | seed.generateFromSchema |
| Local latency | 30-100ms | mock.delay |
| Random local failures | Off | mock.errors |
| GraphQL endpoint | /graphql, disabled until enabled | graphql.enabled: true |
| Falcor endpoint | /model.json, disabled until enabled | falcor.enabled: true |
| Host, port, dev-tool route base, body limit | 127.0.0.1:7331, /__db, 1 MB bodies | server |
Profiles Vs Modes Vs Env Values
Use these names for different jobs:
| Name | Meaning |
|---|---|
profile | Top-level named config policy bundle selected once at config load/startup. |
mode | Scoped behavior inside one API, operation, loader, validator, UI, or integration report. |
env.var | Runtime-supplied non-secret value used to select or fill config. |
env.secret | Runtime-supplied secret value used to fill config, redacted in diagnostics. |
Use a profile when settings must change together and should be reviewable as one
policy. Control-plane deployments are the clearest example: local and production
control planes usually need route exposure, operation strictness, mocks,
audit/durability, and store selection to move together. Prefer
ASYNC_DB_PROFILE=prod-control-plane over many independent flags such as
DB_VIEWER=false, DB_STRICT=true, and DB_STORE=postgres.
import { defineConfig, env } from '@async/db/config';
export default defineConfig({
profile: env.var('ASYNC_DB_PROFILE', { default: 'local-control-plane' }),
profiles: {
'local-control-plane': {
server: {
dataPath: false,
expose: {
rest: 'registered-only',
viewer: 'dev',
schema: 'dev',
manifest: 'dev',
health: 'open',
},
},
operations: { enabled: true, strict: true, acceptRefs: 'ref' },
},
'prod-control-plane': {
mock: { delay: 0, errors: null },
server: {
dataPath: false,
expose: {
rest: 'registered-only',
graphql: false,
viewer: false,
schema: false,
manifest: false,
health: 'open',
},
},
operations: { enabled: true, strict: true, acceptRefs: 'ref' },
stores: {
default: 'postgres',
postgres: {
connectionString: env.secret('DATABASE_URL'),
},
},
},
},
});
Profiles merge over the base config, and inline/programmatic options still merge
last. Profile patches can override static policy such as dbDir, outputs,
stores, resources, schema, mock, server, rest, graphql, falcor,
operations, contracts, and generate. They cannot set loader context keys
such as cwd, configPath, from, fs, profile, or profiles.
Use env.var(...) for selected profile names, host, port, state directory, CI
output paths, and public base URLs:
env.var('ASYNC_DB_PROFILE')
env.var('ASYNC_DB_PROFILE', { default: 'development' })
env.var('ASYNC_DB_PROFILE', { dev: 'development', prod: 'production' }, { default: 'dev' })
Use env.secret(...) for database URLs, admin tokens, encryption keys, and
external credentials:
env.secret('DATABASE_URL')
Do not use top-level mode for deployment policy. mode remains useful inside
scoped APIs that already define what their mode means, such as validation,
schema loading, migrations, UI behavior, and integration reports.
Data Folder (db/)
Use dbDir when data files live somewhere other than ./db:
import { defineConfig } from '@async/db/config';
export default defineConfig({
dbDir: './data',
});
Existing sourceDir configs still work; dbDir is the shorter data-folder name. If both are provided, sourceDir wins for backwards compatibility.
Source And Store Binding
Source data files and runtime persistence are separate concerns. By default, source JSON files stay unchanged and app writes go to the generated JSON store under .db/state.
Use stores.default when every resource should use the same runtime store:
import { defineConfig } from '@async/db/config';
export default defineConfig({
stores: {
default: 'sourceFile',
},
});
With that config, writes to plain JSON resources update db/<resource>.json directly. This is useful for small local web apps where the project folder should contain the saved app state.
Use resources.<name>.store to override the default for one resource:
import { defineConfig } from '@async/db/config';
export default defineConfig({
stores: {
default: 'sourceFile',
},
resources: {
importedRows: {
store: 'json',
indexes: [
{ fields: ['observedAt'] },
{ fields: ['domain', 'observedAt'] },
],
},
},
});
The sourceFile store is intentionally narrow. It is only for resources where supported writebacks should update plain .json source data files. Other supported source formats stay read-only inputs and still hydrate runtime state.
indexes is metadata for store selection, generated tooling, and doctor scale warnings. The default JSON store does not build physical indexes.
Git Remotes And Mirrors
Git-backed resources use git.remotes in db.config.js and source: gitFiles(...) or related helpers in schema files. This keeps connection details out of resource schemas while letting each collection define its own path mapping.
import { defineConfig } from '@async/db/config';
import { githubRemote } from '@async/db/git';
export default defineConfig({
git: {
remotes: {
content: githubRemote({
repo: 'acme/site-content',
branch: 'main',
mode: 'app',
}),
},
},
graphql: { enabled: true },
});
GitHub is the built-in remote type in this slice. mode: "token" can read snapshots directly with a token or public repository access. mode: "app", "actions-pull", and "actions-dispatch" are the long-lived integration paths owned by @async/github-app; pass an injected client or bridge snapshot until that package owns live App/Actions operations in your deployment.
The default Git mirror is the existing JSON state under .db/state with receipt-mode writes. Use SQLite when you want a CMS-like production mirror with faster local reads and a durable write-through outbox:
import { defineConfig } from '@async/db/config';
import { githubRemote } from '@async/db/git';
import { sqliteMirror } from '@async/db/sqlite';
export default defineConfig({
git: {
remotes: {
content: githubRemote({
repo: 'acme/marketing-content',
branch: 'main',
mode: 'app',
}),
},
mirror: sqliteMirror({
file: './.db/git-mirror.sqlite',
writes: 'through',
}),
},
});
Receipt-mode mirrors expect writes to go through a Git driver such as @async/github-app so the mirror updates after a commit or PR receipt. Without that driver, package API, REST, and GraphQL writes to Git-backed resources are rejected instead of silently changing only the mirror.
Generated schema and viewer manifests include only safe source metadata: source kind, remote alias, patterns, read mode, body field, and source shape. They do not emit GitHub tokens, private keys, webhook secrets, app installation ids, or remote client objects.
Optional database stores keep the same data-file and schema workflow while moving
runtime persistence out of .db/state. They store whole resources as JSON
values in v1, so package API, REST, GraphQL, defaults, and source-hash refresh
behavior stay the same:
import { defineConfig } from '@async/db/config';
import { postgresStore } from '@async/db/postgres';
import { redisJson, redisStore } from '@async/db/redis';
export default defineConfig({
resources: {
users: { store: 'postgres' },
sessions: { store: 'redis' },
},
stores: {
postgres: postgresStore({ client: pgPool }),
redis: redisStore({ client: redisClient, prefix: 'my-app:' }),
redisJson: redisJson({ client: redisJsonClient, prefix: 'my-json-app:' }),
},
});
The package does not install database clients for you. Pass a pg Pool/Client
or compatible object to postgresStore({ client }), and pass a Redis-like,
edge KV, Valkey, Dragonfly, or compatible object with get(key) and
set(key, value) to kvStore() or redisStore().
redisJson() is the Redis-backed JSON engine path from @async/json. It stores
collection records under per-record Redis JSON keys and creates physical Redis
Search indexes only from explicit resources.<name>.indexes metadata. Use it
when a JSON-shaped resource should keep the Async DB schema, generated types,
operations, REST, and GraphQL contract while moving the live runtime out of
local sidecar state.
Schema Strictness
Unknown fields in schema-backed data warn by default. Use strict checks when data shape drift should fail:
import { defineConfig } from '@async/db/config';
export default defineConfig({
schema: {
unknownFields: 'error',
},
});
Keep the default warn while data shape is still changing.
Schema JavaScript Modules
.schema.js files are loaded as ESM. If the project root is not already "type": "module", @async/db creates db/package.json with "type": "module" before loading schema files inside the data folder (db/). Aggregate unbundle uses the same rule: it writes generated .schema.js files under db/ when that folder can be loaded as ESM. For custom output folders, add an ESM package boundary or generate .schema.json drafts instead.
Disable that marker when you manage data-folder package metadata yourself:
import { defineConfig } from '@async/db/config';
export default defineConfig({
schema: {
autoModulePackageJson: false,
},
});
Standard Schema Output
@async/db detects Standard Schema validators by shape without installing a
validator dependency. Set schema.standardSchema: true when generated
executable schema files should prefer the validator-first authoring form for
resources that have a Standard Schema validator:
import { defineConfig } from '@async/db/config';
export default defineConfig({
schema: {
standardSchema: true,
},
});
With that option, aggregate bundle/unbundle output can emit
collection(UserSchema, { fields }) or document(SettingsSchema, { fields })
for Standard Schema-backed resources. Resources without a validator keep the
normal Async DB-first shape.
Schema Defaults
Schema defaults apply when creating collection records through the package API, REST, GraphQL, SQLite adapter, and generated Hono SQLite starter. Updates, patches, and document puts preserve omitted fields instead of backfilling defaults; include a field in the write body when you want to change it.
Safe runtime hydration also applies defaults to additive store migrations by default. Disable that separately when existing runtime records should remain untouched:
import { defineConfig } from '@async/db/config';
export default defineConfig({
defaults: {
applyOnCreate: true,
applyOnSafeMigration: false,
},
});
Generated Schema Seed Data
Generate mock runtime records for schema-only resources with empty seed data:
import { defineConfig } from '@async/db/config';
export default defineConfig({
seed: {
generateFromSchema: true,
generatedCount: 5,
},
});
Data files in db/*.json remain the source of truth when present.
Mock Delay And Errors
@async/db delays local responses by 30-100ms by default. Use 0 to disable delay, a number for fixed delay, or a tuple for a range.
import { defineConfig } from '@async/db/config';
export default defineConfig({
mock: {
delay: [50, 300],
errors: {
rate: 0.05,
status: 503,
message: 'Random local mock failure',
},
},
});
Random errors stay off by default. Turn them on when testing retries and error UI.
Mock behavior is a development tool, so the whole mock block (delay and
errors) is skipped automatically when NODE_ENV=production. Set
mock.production: true only when a production-like environment should keep
artificial delays or chaos errors, for example during chaos testing.
async-db doctor --production reports which of the two states applies.
Server Options
Use server for a different host, port, dev-tool route base, or JSON body limit:
import { defineConfig } from '@async/db/config';
export default defineConfig({
server: {
apiBase: '/__db',
dataPath: '/db',
host: '127.0.0.1',
port: 7331,
maxBodyBytes: 1048576,
maxEventClients: 100,
},
});
server.maxEventClients caps concurrent /__db/events live-event subscribers;
extra subscriptions receive a 503 VIEWER_EVENTS_LIMIT error until a slot
frees up. The stream sends a keep-alive comment every 30 seconds so proxies do
not drop idle connections.
GET /__db/health is a readiness probe with its own exposure setting
(server.expose.health, default 'open'), so it stays reachable for load
balancers even when viewer and schema routes are locked down.
server.authorize gates every db-handled request (REST, viewer, schema,
manifest, GraphQL, Falcor, events, health, operations) before it executes.
Return true to allow, false for a 403, or { status, body } for a custom
denial. Requests that fall through to your own app routes never reach the hook:
export default defineConfig({
server: {
authorize({ request, route, method }) {
if (method === 'GET') return true;
return request.headers.authorization === `Bearer ${process.env.DB_WRITE_TOKEN}`;
},
},
});
JSON Store Durability
stores.json.durability: 'wal' gives Redis-style write durability: each write
is acknowledged after its delta is appended (and fsynced per `fsync:
'always' | 'everysec' | 'no'`) to a hidden per-resource log, and the pretty
state file is checkpointed shortly after (checkpointMs, default 250ms).
Boot and reads replay the log tail, so acknowledged writes survive crashes;
checkpoints are versioned. async-db promote configures this for you.
stores.json.durability: 'versioned' keeps a snapshot of the previous state
file on every write under .db/state/.versions/<resource>/, pruned to
maxVersions (default 10). Roll back with async-db restore <resource>, and
bundle full backups with async-db backup:
export default defineConfig({
stores: {
json: {
driver: 'json',
durability: 'versioned',
maxVersions: 10,
},
},
});
Audit Trails
resources.<name>.audit: true appends one JSON line per successful runtime
write to .db/state/.audit/<resource>.jsonl with the op, id, and changed field
names; audit: { values: true } also records before/after snapshots. Audit
failures emit a process warning and never fail the data write.
server.apiBase scopes the local data explorer and internal development routes:
viewer, schema, batch, import, live events, and runtime log. REST
resources such as /users and the standalone GraphQL path stay unchanged unless
you configure those surfaces separately.
server.dataPath scopes the app-facing REST resource alias. It defaults to
/db, so db/users.json is available at GET /db/users.json. Set it to
false to disable the alias and use scoped REST under /__db/rest plus
standalone root REST routes.
Request Tracing
Request tracing is opt-in and safe for local debugging. It records handled DB
HTTP requests, phase timings, route/resource/operation metadata, slow status,
and hook short-circuit metadata when available. It records query keys only, not
query values, request bodies, response bodies, cookie headers, or authorization
headers.
import { defineConfig } from '@async/db/config';
export default defineConfig({
server: {
trace: {
enabled: true,
slowMs: 100,
console: true,
events: true,
header: 'x-async-db-request-id',
},
},
});
Use server.trace: true to enable tracing with defaults:
slowMs: 0, console: true, events: true, and
header: 'x-async-db-request-id'. Set server.trace: false or omit it to keep
tracing disabled.
Trace events are emitted through the runtime log stream, so GET /__db/log
shows request traces alongside normal resource-change events. Console output is
compact:
[async-db] GET /db/users 200 18ms route=rest resource=users op=list requestId=...
[async-db:slow] GET /__db/rest/users/u_1 401 122ms route=hono-rest resource=users op=get hook=beforeRequest shortCircuit=true requestId=...
Explicit integration options such as createDbRuntime({ handler: { trace } }),
createDbRequestHandler(db, { trace }), dbPlugin({ trace }),
createDbHonoApp({ trace }), and
registerDbRoutes(app, db, { trace }) win over db.config.js
server.trace.
Use server.expose when a project wants production-like route hardening.
operations.enabled: true only enables registered operation execution; it does
not automatically lock down raw REST routes. To make registered operations the
only data API path, opt into operation-only exposure with
server.expose.rest: 'registered-only':
import { defineConfig } from '@async/db/config';
export default defineConfig({
outputs: {
operationRegistry: './src/generated/db.operations.json',
operationRefs: './src/generated/db.operation-refs.json',
},
operations: {
enabled: true,
acceptRefs: 'ref',
},
server: {
expose: {
rest: 'registered-only',
graphql: false,
viewer: 'dev',
schema: 'dev',
manifest: 'dev',
},
},
});
Exposure values are open, registered-only, dev, disabled, and false.
dev routes are available unless NODE_ENV=production. registered-only is
not a general hardening switch. For REST it specifically means raw REST resource
and batch routes are blocked, while POST /__db/operations/:ref can still
execute registered operation templates.
registered-only does not make @async/db decide what production means for an
app. If a project wants startup and async-db doctor to fail when registered
operations are missing or unresolved, set operations.strict: true:
export default defineConfig({
operations: {
enabled: true,
strict: true,
acceptRefs: 'ref',
},
server: {
expose: {
rest: 'registered-only',
},
},
});
With operations.strict: true, the built-in server and doctor require
registered operations to be enabled and resolvable through
operations.registry, operations.resolveRef, outputs.operationRegistry /
operations.outFile, or operation files under operations.sourceDir. Missing,
invalid, or empty operation sources fail early with
OPERATIONS_STRICT_MODE_WITHOUT_OPERATIONS. For public operation-only APIs,
prefer operations.acceptRefs: 'ref'; doctor reports this as non-blocking
guidance only when operation strict mode is enabled.
outputs.viewerManifest and async-db viewer manifest include safe summaries
for the same route exposure policy. The manifest reports store families,
resource store mappings, route exposure values, registered operation
availability, and resource action availability. It intentionally reports only
safe booleans and labels: no connection strings, raw clients, state paths,
source hashes, request bodies, response bodies, auth headers, cookie headers, or
server operation templates are included.
Registered Queries
Registered queries are optional allowlisted REST or GraphQL request templates.
The config and CLI still use the operations name. Operation sources live under
operations.sourceDir, which defaults to ./db/operations. When that folder
is inside the data folder (db/), @async/db reserves it for operation templates and
does not load it as data. Move it elsewhere by changing
operations.sourceDir.
db/operations/get-user.json
{
"name": "GetUser",
"method": "GET",
"path": "/users/{id}.json",
"query": {
"select": "id,name"
}
}
GraphQL templates use the same registry:
{
"name": "GetUser",
"query": "query GetUser($id: ID!) { user(id: $id) { id name } }",
"operationName": "GetUser",
"variables": {
"id": "{id}"
}
}
async-db operations build
outputs.operationRegistry receives the full server registry with templates.
outputs.operationRefs receives client-safe refs with names and callable refs
only. The client-exposed surface is just operations.<name>.name and
operations.<name>.ref; paths, query templates, variables, request bodies, and
server registry contents stay out of that client file. Generated refs default
to hashOperation(template) unless the operation source provides an explicit
ref. operations.acceptRefs controls which
identifiers the server accepts by default: 'ref', 'name', or 'both'. Use
operations.validateRef or operations.resolveRef only when an app-owned
runtime needs custom server-side policy or a custom registry lookup.
Manual inline registries can use operation objects or string REST templates,
for example { GetUser: '/users/{id}.json?select=id,name' }.
Operation names and refs must be unique; the build fails
instead of generating client refs that could point at the wrong operation.
If outputs.operationRegistry is missing, invalid, or points at the client-safe
db.operation-refs.json file instead of the server db.operations.json
registry, registered operation execution returns
OPERATION_REGISTRY_LOAD_FAILED; rebuild the registry or fix the configured
path before treating operation misses as missing refs.
Production Doctor Checks
async-db doctor --production adds production-readiness findings for
JSON-backed resources. It keeps ordinary local prototype checks quiet by
default, then warns when production JSON resources do not have explicit schema
files and emits review guidance for keeping JSON-backed production resources
small, low-write, single-writer, and backed up.
Use async-db doctor --production --usage ./src to add static app-usage
findings for endpoint exposure choices. The usage scanner reads source text and
emits a db.usageManifest; it does not execute app files.
Use strict production checks in CI when those warnings should fail:
async-db check --strict --production
For CI review, async-db operations contract prints a deterministic
client-exposed contract with generatedAt removed. Commit the approved refs or
contract file, then run:
async-db operations contract --check
--check compares against outputs.operationRefs by default, or against
--out <file> when provided, and fails when the exposed operation names or refs
change.
Runtime Forks
Runtime forks are package API state, not db.config.js data folders. Use db.forks.create(), db.forks.open(), db.forks.ensure(), branches, snapshots, migrations, and routing when an app needs tenants, previews, debug copies, or upgrade flows.
const tenant = await db.forks.ensure('tenant_acme', {
from: 'main',
metadata: { purpose: 'tenant' },
});
const draft = await tenant.branches.ensure('draft', {
from: 'main',
metadata: { purpose: 'draft' },
});
The old data-folder forks and templates config surfaces were removed so fork has one meaning: an isolated logical database instance.