Server And Local Data Explorer
async-db serve starts a local development server. It syncs on startup, watches data files in db/, serves REST and GraphQL endpoints, and exposes the built-in local data explorer.
Local Trust Boundary
The server binds to 127.0.0.1 by default. It is intended for local development and tests, not public hosting.
Important write surfaces:
- REST writes update runtime state.
- GraphQL mutations update runtime state.
- Explorer CSV import writes CSV files into the configured
dbDir. - Resources bound to the
sourceFilestore may write supported changes back to source JSON files.
Config and schema JavaScript are trusted project code. Do not treat .schema.js, .schema.js, or config hooks as untrusted data.
Request Tracing
Enable request tracing when a local page appears stuck or slow and you need to
see where a DB request spent time:
import { defineConfig } from '@async/db/config';
export default defineConfig({
server: {
trace: true,
},
});
Tracing adds an x-async-db-request-id response header, emits
request-trace events through GET /__db/log, and prints concise console
lines unless disabled. Traces include method, pathname, query keys, route
family, resource, operation, id when known, status, handled state, duration,
slow status, safe error code/message, and phase timings for clear boundaries
such as route matching, mock behavior, REST handling, Hono hooks, collection or
document reads/writes, response shaping, registered operations, batch items, and
fork dispatch.
Trace data intentionally omits request bodies, response bodies, cookie headers,
authorization headers, and query values. Query strings are represented as keys
only, for example ["select", "expand"].
Local Data Explorer
Open the built-in local data explorer after starting the server:
http://127.0.0.1:7331/__db
The local data explorer opens at the default dev-tool route base, /__db. Change it with
server.apiBase when an app needs a different reserved path; schema, batch,
import, events, log, and fork routes move with that base.
Opening http://127.0.0.1:7331/ in a browser shows a small index with links to the local data explorer, schema, GraphQL endpoint, and resource routes. API-style requests to / keep returning JSON discovery data by default.
The explorer is a dark, dense local database-console shell. It keeps a narrow
app rail for major areas, a contextual resource explorer, and a main workspace
that switches panels without writing project files.
The app rail includes:
- Connections for the local
@async/dbruntime and safe route summaries - Data for resource grids, JSON detail, API, relations, and diagnostics
- Query for REST and GraphQL examples/runners, with SQL and Explain disabled
unless a future safe capability explicitly enables them
- Schema for generated schema and field inspection
- Operations for registered operation availability summaries
- Logs for local event/log endpoints and source diagnostics
- Settings for browser-local viewer state and safe store summaries
The contextual explorer groups real manifest objects, including collections,
documents, stores, recent browser-local resources, and diagnostics. Resource
rows use manifest-provided store mappings and action availability to show
read/write state; they do not include seed records or fake activity.
The Data area fetches records through the manifest resource routes. Collections
load a bounded page with offset and limit query parameters, show schema
fields before drift keys found on the loaded page, mark identity fields, render
nested arrays/objects through detail actions, and use relation hints for related
record links. Documents render as a document workspace instead of a collection
grid.
Write controls are driven by the manifest action summaries. Read-only, static,
registered-only, or disabled resources show disabled write state instead of
active create/patch/delete controls. When a resource is writable, edits are
staged in browser state from the selected JSON panel and Apply/Discard controls
appear only while the staged value differs from the loaded record. Applying an
edit calls the existing REST route for that resource so normal validation,
route exposure, policy, and store behavior still own the result.
The Query area has five modes:
- Resource builds REST-shaped read URLs from the selected resource route,
select, expand, offset, and limit. It disables execution when direct
REST is disabled or configured as registered-only.
- Operation posts variables to the public registered-operation endpoint/ref
shape when operations are enabled and client-safe refs or summaries exist.
The viewer does not receive or render server operation templates.
- GraphQL uses the configured GraphQL endpoint only when GraphQL is enabled and
exposed. It supports a query editor, variables editor, optional operation
name, SDL loading, and examples derived from resource metadata.
- SQL and Explain are visible product slots but disabled by default. A driver
name such as SQLite or Postgres is not enough to enable arbitrary SQL or plan
inspection; a future safe capability contract must opt in explicitly.
The Schema area inspects only the manifest and generated schema payloads. It
shows resource kind, identity, store mapping, validation/unknown-field summary,
field types, required/nullable/default/enum metadata, read-only or
derived/computed flags, relation hints, and UI hints when those are already in
the manifest/schema output.
The Logs area combines safe local diagnostics and session activity:
- manifest diagnostics;
- live resource events from the configured events route when live events are
available;
- request trace rows from the runtime log stream when the user connects it;
- import status summaries;
- batch request status summaries.
Log rows are classified as error, warning, info, live event, or request trace
and are rendered as summaries. They do not include request bodies, response
bodies, auth headers, cookies, source paths, runtime state paths, connection
strings, raw client objects, or source hashes.
The Settings area is read-only. It shows viewer routes, response formats,
custom viewer links, route exposure, store registry summaries, resource store
mappings, import/GraphQL/Falcor/operation availability, and browser-local keys.
Driver cards are capability summaries only; they do not install plugins or
mutate project config.
The explorer still includes drag-and-drop CSV import into the configured data
folder (db/), REST specs with copyable examples, a REST request runner,
GraphQL SDL and operation references, schema and field inspection, and source
diagnostics when one data file is broken.
Custom Viewer Manifest
The built-in local data explorer reads the same JSON manifest that custom viewer UIs can use:
GET /__db/manifest
GET /__db/manifest.json
GET /__db/manifest.html
GET /__db/manifest.md
/manifest.json returns JSON. /manifest.html returns the built-in formatted JSON viewer with dark mode by default, dark/light/system theme controls, copy, and pretty/raw formatting controls. /manifest.md returns Markdown with the manifest JSON in a fenced code block for AI clients. /manifest chooses from registered response formats using the request Accept header. If server.apiBase changes, the routes move with it, for example GET /_db/manifest.
The manifest includes:
- API links for the local data explorer, manifest, manifest JSON/HTML/Markdown routes, schema, events, batch, import, GraphQL, and each REST resource
- built-in and configured custom viewer links
- resource and field metadata, including generated UI hints and relation hints
- safe store summaries, effective resource store mappings, write modes, and capability booleans
- route exposure summaries for REST, viewer, schema, manifest, GraphQL, Falcor, and registered operations
- registered operation availability, accepted ref mode, contract names, and whether client-safe refs are configured
- resource action availability for read, create, patch, delete, replace, batch, CSV import, operations, and GraphQL
- active query modes a viewer can safely enable for each resource
- UI capabilities such as writes, batching, CSV import, GraphQL, and live events
- diagnostics suitable for display in a custom UI
The manifest does not include seed records, source paths, source hashes, runtime
state paths, raw client objects, connection details, server operation
templates, request bodies, response bodies, auth headers, cookie headers, or
GraphQL SDL. Custom viewers should fetch manifest.json for UI metadata and
route links, then fetch actual records from REST, GraphQL, or registered
operations only when the manifest says those modes are available. api.formats
lists the registered response formats, media types, and manifest paths for
custom viewers and tools.
Add custom viewer links when a project ships its own data UI:
Override the built-in Markdown renderer when a project needs a different shape:
import { defineConfig } from '@async/db/config';
import { stringify as stringifyYaml } from 'yaml';
export default defineConfig({
server: {
viewerLinks: [
{ label: 'App Data Viewer', href: 'http://127.0.0.1:5173/db' },
],
},
});
You can also write the same shape to a committed artifact:
import { defineConfig } from '@async/db/config';
export default defineConfig({
outputs: {
viewerManifest: './src/generated/db.viewer.json',
},
});
async-db viewer manifest --out ./src/generated/db.viewer.json
REST Routes
REST routes are enabled by default. Set rest.enabled: false to turn off
generated resource routes and REST batching while keeping dev-tool routes such
as the local data explorer, schema, manifest, import, events, and GraphQL available.
The app-facing REST route base defaults to /db, matching the data folder (db/).
For a data file at db/users.json, fetch the synced runtime resource with:
const users = await fetch('/db/users.json').then((response) => response.json());
Scoped REST remains available under the tool route base, such as
GET /__db/rest/users.json. Standalone async-db serve also keeps root REST
routes such as GET /users for local convenience. Set server.dataPath: false
to disable only the /db alias.
When a prototype route needs to become an app-owned API route, see the
Prototype To Production REST Guide for
/api/db/*, /api/*, registered operation refs, and route lockdown.
File-Like .json Routes
Use .json routes when you want the URL to resemble the source data file path:
db/users.json becomes GET /db/users.json. The server still reads the
synced runtime resource, not the source JSON file directly, so local writes
continue going to the selected runtime store.
Collections can use .json for list and record reads:
GET /db/users.json
GET /db/users/u_1.json
Singleton documents can use the same file-like read shape:
GET /db/settings.json
Use ?id= only with the explicit collection .json route:
GET /db/users.json?id=u_1
Extensionless REST routes keep normal REST semantics and return a structured
error for ?id=. Use GET /db/users/u_1.json or GET /db/users/u_1 there.
Query options such as select, expand, offset, and limit apply before
the response format renders. Sibling .html and .md routes use the same
shaped data for browser or Markdown views.
Collections:
GET /db/users.json
GET /db/users/:id.json
POST /db/users
PATCH /db/users/:id
DELETE /db/users/:id
Singleton documents:
GET /db/settings.json
PUT /db/settings
PATCH /db/settings
REST examples:
curl http://127.0.0.1:7331/db/users.json
curl 'http://127.0.0.1:7331/db/users.json?select=id,name&offset=0&limit=20'
curl 'http://127.0.0.1:7331/db/users.json?id=u_1&select=id,name'
curl http://127.0.0.1:7331/db/users/u_1.json
curl http://127.0.0.1:7331/db/settings.json
curl -X POST http://127.0.0.1:7331/db/users \
-H 'content-type: application/json' \
-d '{"id":"u_2","name":"Grace Hopper","email":"grace@example.com"}'
curl -X PATCH http://127.0.0.1:7331/db/users/u_2 \
-H 'content-type: application/json' \
-d '{"name":"Rear Admiral Grace Hopper"}'
curl -X DELETE http://127.0.0.1:7331/db/users/u_2
Schema-backed computed fields are resolved only when selected. For example,
GET /db/users/u_1.json?select=id,fullName calls the trusted resolver registered
by field.computed(...); default reads continue returning only stored data
fields.
REST Formats
Resource GET routes return JSON by default. The explicit .json extension uses the same shaped data:
GET /db/users
GET /db/users.json
GET /db/users.html
GET /db/users.md
GET /db/users/u_1
GET /db/users/u_1.json
GET /db/users/u_1.html
GET /db/users/u_1.md
.json, .html, and .md are built in. Config entries with the same extension override the built-in resource renderer, and object entries can also override manifest rendering. Extensionless resource and manifest routes negotiate registered media types from Accept; unsupported Accept values fall back to the configured default format. Format renderers receive data after normal REST shaping, so select, expand, offset, and limit apply before rendering.
import { defineConfig } from '@async/db/config';
export default defineConfig({
rest: {
formats: {
default: 'json',
md({ resourceName, data }) {
return {
body: `# ${resourceName}\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`\n`,
contentType: 'text/markdown; charset=utf-8',
};
},
yaml: {
mediaTypes: ['application/yaml', 'text/yaml'],
contentType: 'application/yaml; charset=utf-8',
render({ data }) {
return stringifyYaml(data);
},
renderManifest({ data }) {
return stringifyYaml(data);
},
},
},
},
});
Function shorthand is resource-only for compatibility. Use object syntax when a format needs media-type negotiation or manifest support, such as GET /__db/manifest.yaml. @async/db does not execute .jsx routes directly; JSX is a source/runtime choice for your renderer, while .html is the response format.
Relationship Expansion
Schema-backed scalar fields can declare relation metadata while data files keep plain ids:
{
"authorId": {
"type": "string",
"required": true,
"relation": {
"name": "author",
"to": "users",
"toField": "id",
"cardinality": "one"
}
}
}
Then REST can expand that to-one relation:
curl 'http://127.0.0.1:7331/db/posts/p_1.json?expand=author&select=id,title,author.name'
select supports top-level fields and one nested expanded relation field. Relation expansion is depth 1 in this MVP. Reverse to-many expansion is intentionally deferred.
REST Batching
REST batching is supported through the scoped tool endpoint and the standalone
development alias:
POST /__db/batch
POST /batch
If server.apiBase is changed, the batch endpoint follows that base, for
example POST /_db/batch.
[
{
"method": "GET",
"path": "/db/users.json"
},
{
"method": "PATCH",
"path": "/db/settings",
"body": {
"theme": "dark"
}
}
]
REST batches execute sequentially and are intentionally non-transactional. If an earlier write succeeds and a later batch item fails, the earlier write stays committed.
Errors are shaped for humans and automation:
{
"error": {
"code": "REST_BATCH_INVALID_PATH",
"message": "REST batch path must start with \"/\": users",
"hint": "Use absolute local paths such as \"/users\", \"/settings\", or \"/__db/schema\".",
"details": {
"path": "users"
}
}
}
Canonical Resource Routes And Bulk Processing
Standalone async-db serve also exposes canonical resource aliases:
GET /resources/users
GET /resources/users/:id
POST /resources/users
PATCH /resources/users/:id
DELETE /resources/users/:id
These aliases use the same runtime resources, schema validation, response
formats, and write behavior as /db/*, root REST, and /__db/rest/*.
Bulk create sends an array of records to the collection route:
curl -X POST http://127.0.0.1:7331/resources/users \
-H 'content-type: application/json' \
-d '[{"id":"u_1","name":"Ada"},{"id":"u_2","name":"Grace"}]'
Bulk patch can apply one patch to several ids:
{
"ids": ["u_1", "u_2"],
"patch": {
"active": false
}
}
It can also accept per-record patch items:
[
{ "id": "u_1", "patch": { "name": "Ada Lovelace" } },
{ "id": "u_2", "patch": { "name": "Grace Hopper" } }
]
Bulk replace uses PUT /resources/users with a records array. Only listed ids
are replaced; unlisted records are preserved. Replacement records must satisfy
the resource schema.
Bulk delete accepts repeated query ids or a body:
DELETE /resources/users?id=u_1&id=u_2
{ "ids": ["u_1", "u_2"] }
Bulk operations execute sequentially and do not roll back earlier successful
items. Responses include per-item status and a summary:
{
"results": [
{ "index": 0, "id": "u_1", "status": 200, "body": { "id": "u_1" } }
],
"summary": { "ok": 1, "errors": 0 }
}
Registered REST Operations
Registered queries are optional REST or GraphQL request templates with callable
refs and optional names. They let production-style apps allowlist specific
reads and writes while local data-file CRUD can stay open by default. The
underlying config and CLI still use the operations name.
GET /users/{id}.json?select=id,name
{
"name": "GetUser",
"method": "GET",
"path": "/users/{id}.json",
"query": {
"select": "id,name"
}
}
GraphQL templates use the same registry and ref execution route:
{
"name": "GetUser",
"query": "query GetUser($id: ID!) { user(id: $id) { id name } }",
"operationName": "GetUser",
"variables": {
"id": "{id}"
}
}
Build the registry and optional client-safe refs:
async-db operations build --out ./src/generated/db.operations.json --refs-out ./src/generated/db.operation-refs.json
At runtime, registered operation execution is always a POST to the dev-tool base:
curl -X POST http://127.0.0.1:7331/__db/operations/users.get \
-H 'content-type: application/json' \
-d '{"variables":{"id":"u_1"}}'
Readable names work as app-facing operation refs. Refs are allowlist identifiers, not
secrets. Generated refs default to hashOperation(template), and apps can set
their own ref in operation sources when they want readable or app-owned ids.
Keep the full registry server-side. Client refs contain names and callable refs
but not full request templates, variables, or request bodies. Use
async-db operations contract --check in CI when committed refs need approval
before the browser-facing operation contract changes. operations.acceptRefs
controls whether the server accepts refs, names, or both. Use operations.resolveRef or
operations.validateRef for custom server-side registry lookup or policy.
To block raw REST while allowing registered operations:
import { defineConfig } from '@async/db/config';
export default defineConfig({
outputs: {
operationRegistry: './src/generated/db.operations.json',
},
operations: {
enabled: true,
acceptRefs: 'ref',
},
server: {
expose: {
rest: 'registered-only',
graphql: false,
viewer: 'dev',
schema: 'dev',
manifest: 'dev',
},
},
});
registered-only blocks raw REST resource and batch routes. Registered REST
operation execution still uses normal REST shaping, including select,
formats, schema validation, and computed resolver projection. Registered
GraphQL operations execute through the same GraphQL executor as direct GraphQL
requests, and still require graphql.enabled !== false.
The viewer manifest mirrors these exposure choices under routeExposure and
per-resource actions. For example, server.expose.rest: 'registered-only'
keeps registered operations available but marks direct resource reads and batch
requests unavailable with the registered-only reason. A custom viewer should
disable controls from those summaries before issuing a request.
GraphQL Boundary
GraphQL is available at /graphql when you opt in with graphql.enabled: true.
A scoped alias is also available at /__db/graphql for embedded dev servers.
It supports aliases, variables, operationName, __typename, named and inline
fragments, @include/@skip, HTTP batching, and minimal
__schema/__type introspection for local tooling.
Set graphql.enabled: false when an app wants REST, schema, manifest, the local
data explorer, import, and events without a GraphQL endpoint. GraphQL is disabled
by default so the starter surface stays REST-first.
GraphQL HTTP batches execute sequentially and are intentionally non-transactional. If an earlier mutation succeeds and a later batch item fails, the earlier mutation stays committed.
REST remains the documented happy path because REST plus the local data explorer is the intended default workflow.
GraphQL selections use the same read projection/fanout path as REST for computed
fields. Registered GraphQL operations are fixed query templates; they are not a
direct database query language and do not expose backend SQL or Redis commands.
Use server.expose.graphql, server.expose.viewer, server.expose.schema, and
server.expose.manifest to keep non-REST surfaces open, dev, or disabled in
production-like servers.
Unsupported in v1:
- subscriptions
- full GraphQL spec introspection
- general-purpose GraphQL validation beyond @async/db's local subset
- relation traversal from schema relation metadata; GraphQL projects stored fields in v1
Falcor Boundary
Falcor is available at /model.json when you opt in with falcor.enabled: true
for browser clients using falcor.HttpDataSource('/model.json'). A scoped alias
is also available at /__db/model.json for embedded dev servers. Falcor is
disabled by default.
Supported v1 behavior:
get(pathSets)
set(jsonGraphEnvelope)
call(functionPath, args)
Collections expose JSONGraph list refs and by-id maps:
users.length
users[0] -> usersById.u_1
usersById.u_1.name
Singleton documents expose normal nested document paths, such as
settings.theme.
Falcor set is intentionally direct: it updates collection fields or document
paths through normal @async/db runtime writes and schema validation, then returns
the post-write JSONGraph for the written paths. Creates, deletes, reorders, and
multi-step workflows should use call.
Falcor call maps to registered operations:
{
"method": "call",
"callPath": ["operations", "users.get"],
"arguments": [{ "id": "u_1" }]
}
This executes the registered operation ref or name users.get. If the operation
returns a JSONGraph envelope, @async/db passes it through. Otherwise the result
is wrapped under operations.users.get.result.
Set falcor.enabled: false when an app wants REST, GraphQL, schema, manifest,
the local data explorer, import, and events without a Falcor endpoint.
Watch Behavior
serve watches data files in db/, ignores .db/, reloads valid resources when files change, and surfaces file-specific diagnostics in the local data explorer without breaking unrelated resources.
If an app commits generated files under frontend source folders, Vite may still reload when those files genuinely change. Only ignore generated files that the browser does not need to hot reload.