Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Other useful paths:
- [`examples/computed-fields`](./examples/computed-fields): computed field patterns across several schema-backed models.
- [`examples/production-json`](./examples/production-json): feature flags and settings in the JSON store behind registered operations.
- [`examples/rest-client`](./examples/rest-client): calling @async/db from app or test code.
- [`examples/local-web-app`](./examples/local-web-app): loopback app state saved directly to `db/*.json`.
- [`examples/schema-manifest`](./examples/schema-manifest): schema metadata for admin/CMS UI.
- [`examples/standard-schema`](./examples/standard-schema): Standard Schema validators with Async DB metadata overlays.
- [`examples/hono-auth`](./examples/hono-auth): optional Hono auth and write hooks.
Expand Down Expand Up @@ -167,6 +168,55 @@ The built-in JSON store is production-appropriate only for file-suitable resourc

@async/db is not an auth layer, an ORM, a hosted database service, or a broad JSON Schema compatibility project. For production-facing APIs, put app traffic behind registered operations, app-owned auth/authorization, rate limits, and observability. See [Production JSON Database](./docs/json-production.md), [Resource Graduation And Mixed Stores](./docs/store-graduation.md), and [Prototype To Production REST Guide](./docs/prototype-to-production.md).

## Save Directly To `db/*.json`

The default `json` store keeps source fixtures unchanged and writes app edits to
the generated mirror under `.db/state`. For small local apps where saved state
should live in the project folder, use the `sourceFile` store:

```js
import { defineConfig } from '@async/db/config';

export default defineConfig({
stores: {
default: 'sourceFile',
},
});
```

Now writes to plain JSON resources update `db/<resource>.json` directly.
Override individual resources when some data should still use the mirror:

```js
export default defineConfig({
stores: {
default: 'sourceFile',
},
resources: {
importedRows: { store: 'json' },
},
});
```

See [`examples/local-web-app`](./examples/local-web-app) for a loopback app that
saves on blur/change, keeps server state canonical, and uses browser storage
only for transient reload recovery.

For simple local websites, keep the shape boring:

```txt
db/ saved JSON documents and seed data
src/ browser HTML, CSS, and app code
server/ loopback request handlers and @async/db mounting
framework/ small reload, draft, and DOM helpers
```

Run `async-db sync` in that loop even when every resource uses `sourceFile`.
Sync still validates the fixture folder, infers the schema, and writes generated
metadata/types for tools. The difference is that app writes go back to plain
`db/*.json` instead of the `.db/state` mirror, so the project folder contains
the state you want to save, copy, or commit.

## Add Schema When It Pays For It

Data-first fixtures are enough until the shape matters. Inspect what @async/db infers:
Expand Down Expand Up @@ -389,7 +439,7 @@ routes, see the

## Which Example Should I Start With?

The examples are a learning path. Run any example with `npm run db -- sync --cwd ./examples/<name>` and `npm run db -- serve --cwd ./examples/<name>`, or run `npm run examples` to open one lazy examples index. The examples index binds to `127.0.0.1` by default; use `npm run examples -- --tailscale-serve` when you want Tailscale Serve to proxy that local port over HTTPS for devices in your tailnet.
The examples are a learning path. Run most examples with `npm run db -- sync --cwd ./examples/<name>` and `npm run db -- serve --cwd ./examples/<name>`, or run `npm run examples` to open one lazy examples index. Use `npm run examples` for examples with custom app routes such as `local-web-app` and `schema-ui`. The examples index binds to `127.0.0.1` by default; use `npm run examples -- --tailscale-serve` when you want Tailscale Serve to proxy that local port over HTTPS for devices in your tailnet.

| If you want to learn... | Start with | What it shows |
| --- | --- | --- |
Expand All @@ -399,6 +449,7 @@ The examples are a learning path. Run any example with `npm run db -- sync --cwd
| Different computed field patterns | [`examples/computed-fields`](./examples/computed-fields) | Shorthand resolvers, `resolveMany`, formatting, and runtime-context lookups |
| Contract-first resources | [`examples/schema-first`](./examples/schema-first) | Schema-only resources, empty seed records, committed types |
| Calling @async/db from app or test code | [`examples/rest-client`](./examples/rest-client) | `createDbClient`, direct REST calls, REST batching |
| Local app state saved in the project | [`examples/local-web-app`](./examples/local-web-app) | `stores.default: 'sourceFile'`, blur/change saves, transient reload state, custom example runtime |
| Related local records | [`examples/relations`](./examples/relations) | Relation metadata, `expand`, and nested `select` |
| CSV as the source of truth | [`examples/csv`](./examples/csv) | CSV inference, source hashes, mirror refreshes |
| Admin/CMS-style field metadata | [`examples/schema-manifest`](./examples/schema-manifest) | `outputs.schemaManifest` and manifest customization |
Expand Down
7 changes: 2 additions & 5 deletions db.config.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
// Fixture source folder. Defaults to './db'.
dbDir: './db',

// Generated output locations. Most committed outputs are opt-in.
outputs: {
stateDir: './.db',
Expand Down Expand Up @@ -33,8 +30,8 @@ export default defineConfig({

// Runtime stores. The default json store writes app edits to
// .db/state/<resource>.json while keeping source fixtures unchanged.
// Bind a resource to sourceFile only when supported writebacks should update
// a plain .json source fixture. Optional database stores such as
// Set stores.default to sourceFile when every plain .json resource should
// save directly back into db/<resource>.json. Optional database stores such as
// @async/db/postgres, @async/db/kv, and @async/db/redis accept injected
// clients so the core package stays dependency-light.
stores: {
Expand Down
25 changes: 18 additions & 7 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ See [db.config.example.mjs](../db.config.example.mjs) for a commented config wit
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',

outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down Expand Up @@ -160,7 +158,7 @@ Use `dbDir` when fixtures live somewhere other than `./db`:
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
dbDir: './fixtures',
});
```

Expand All @@ -170,18 +168,31 @@ Existing `sourceDir` configs still work; `dbDir` is the shorter fixture-folder n

Source fixtures and runtime persistence are separate concerns. By default, source fixtures stay unchanged and app writes go to the generated JSON store under `.db/state`.

Use `resources.<name>.store` to bind a resource to a different store:
Use `stores.default` when every resource should use the same runtime store:

```js
import { defineConfig } from '@async/db/config';

export default defineConfig({
stores: {
default: 'json',
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. JSONC and CSV files remain source inputs; they cannot use `sourceFile` as writable state.

Use `resources.<name>.store` to override the default for one resource:

```js
import { defineConfig } from '@async/db/config';

export default defineConfig({
stores: {
default: 'sourceFile',
},
resources: {
users: { store: 'sourceFile' },
activityEvents: {
importedRows: {
store: 'json',
indexes: [
{ fields: ['observedAt'] },
Expand Down
1 change: 0 additions & 1 deletion docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ import { createDbHonoApp } from '@async/db/hono';

const app = new Hono();
app.route('/api', await createDbHonoApp({
dbDir: './db',
storage: {
kind: 'sqlite',
file: './data/app.sqlite',
Expand Down
1 change: 0 additions & 1 deletion docs/package-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ pnpm db serve
import { openDb } from '@async/db';

const db = await openDb({
dbDir: './db',
outputs: {
stateDir: './.db',
},
Expand Down
1 change: 0 additions & 1 deletion examples/advanced/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/basic/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/computed-fields/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/content-collections/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/csv/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/data-first/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/diagnostics/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
1 change: 0 additions & 1 deletion examples/hono-auth/db.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
dbDir: './db',
outputs: {
stateDir: './.db',
types: './.db/types/index.d.ts',
Expand Down
77 changes: 77 additions & 0 deletions examples/local-web-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Local Web App Example

## What This Teaches

Use this pattern for small local tools where the Node server is the source of
truth and the app state should be easy to save with the project. The example
writes directly to `db/appState.json` instead of the default `.db/state` mirror.

## Files To Inspect

- [db.config.mjs](./db.config.mjs): sets `stores.default` to `sourceFile`.
- [db/appState.json](./db/appState.json): the saved app document.
- [src/app.js](./src/app.js): browser code that saves on blur/change.
- [framework/state.js](./framework/state.js): transient reload-state helpers.
- [server/runtime.js](./server/runtime.js): app routes, version polling endpoint, and `/__db` mounting.
- [serve-example.mjs](./serve-example.mjs): examples launcher hook.

## Simple Website Structure

The example keeps the important parts in predictable folders:

```txt
db/ saved JSON documents and seed data
src/ browser HTML, CSS, and app code
server/ loopback request handlers and @async/db mounting
framework/ small reload, draft, and input-state helpers
```

Use this shape when a toy website only needs local server state and a little
browser behavior. `src/` stays focused on the page, `server/` owns the loopback
routes, and `framework/` keeps supporting reload and draft helpers out of the
main app file.

## Run It

From the repository root:

```bash
npm run db -- sync --cwd ./examples/local-web-app
npm run examples
```

Open the `local-web-app` link from the examples index. Edit either field, then
move focus out of the field. The app saves the document through the loopback
server into `db/appState.json`.

## Why Sync With `sourceFile`?

`npm run db -- sync --cwd ./examples/local-web-app` still matters even though
the app writes directly to `db/appState.json`. Sync validates the fixture folder,
infers the schema, and writes generated metadata/types for the viewer and tools.

`stores.default: 'sourceFile'` changes where runtime writes land. Instead of
copying app edits into `.db/state/appState.json`, the server writes the plain
JSON source file. That keeps the saved state next to the toy project so it is
easy to inspect, copy, or commit.

## Features To Notice

- `stores.default: 'sourceFile'` makes plain JSON resources writable in `db/`.
- `resources.<name>.store` can still override the default for individual resources.
- Typing only updates transient browser state; blur/change commits to the server.
- Browser storage is only used to restore scroll position, active field, cursor
position, and unsaved draft text during a reload.
- The app polls `/api/version` and reloads when the served app files change.

## Cleanup

This example intentionally edits `db/appState.json` when you use the app. Revert
that file if you want the initial sample text again.

Generated `.db/` output is ignored by git and can be removed whenever you want a fresh mirror.

## More Docs

- [Configuration](../../docs/configuration.md)
- [Server And Viewer](../../docs/server-and-viewer.md)
7 changes: 7 additions & 0 deletions examples/local-web-app/db.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from '@async/db/config';

export default defineConfig({
stores: {
default: 'sourceFile',
},
});
5 changes: 5 additions & 0 deletions examples/local-web-app/db/appState.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Local App Notes",
"note": "Edit this note in the local app. It saves to db/appState.json when the field loses focus.",
"updatedAt": "2026-06-03T00:00:00.000Z"
}
5 changes: 5 additions & 0 deletions examples/local-web-app/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Local Web App",
"description": "A tiny loopback web app that treats @async/db server state as canonical, saves form edits on blur, and writes directly to db/*.json.",
"tags": ["local-app", "source-file", "live-reload"]
}
Loading