@agent-native/core 0.141.4 → 0.141.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
@@ -42,22 +42,22 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
42
42
  avgEvalScore: number;
43
43
  } | {
44
44
  error?: undefined;
45
- ok?: undefined;
46
45
  summary: import("./types.js").TraceSummary;
47
46
  spans: import("./types.js").TraceSpan[];
48
47
  id?: undefined;
48
+ ok?: undefined;
49
49
  } | {
50
50
  error?: undefined;
51
- ok?: undefined;
52
51
  summary?: undefined;
53
52
  spans?: undefined;
54
53
  id: string;
55
- } | {
56
54
  ok?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  id?: undefined;
60
59
  error: any;
60
+ ok?: undefined;
61
61
  } | {
62
62
  error?: undefined;
63
63
  summary?: undefined;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
package/docs/AGENTS.md CHANGED
@@ -71,7 +71,7 @@ preserve public action, state, auth, and agent-chat runtime contracts.
71
71
  | Expose tools to external agents | `content/external-agents.md`, `content/mcp-protocol.md`, `content/mcp-apps.md`, `content/mcp-clients.md` |
72
72
  | Add integrations, setup, or secrets | `content/onboarding.md`, `content/workspace-connections.md`, `content/security.md` |
73
73
  | Build extensions or custom widgets | `content/extensions.md`, `content/agent-web-surfaces.md` |
74
- | Deploy or configure hosting | `content/deployment.md`, `content/server.md` |
74
+ | Deploy or configure hosting | `content/deployment.md`, `content/server-overview.md` |
75
75
  | Write agent instructions or skills | `content/skills-guide.md`, `content/writing-agent-instructions.md` |
76
76
 
77
77
  ## Rules
@@ -90,7 +90,7 @@ page. For the full surface map, see [Agent Surfaces](/docs/agent-surfaces).
90
90
 
91
91
  If the UI and agent both need to do something, reach for an action — not a custom
92
92
  route. For when a route-shaped protocol _is_ the right call, see [Prefer Actions
93
- For App Operations](/docs/server#actions-first).
93
+ For App Operations](/docs/server-overview#actions-first).
94
94
 
95
95
  On this page: [Start with one action](#hello-action), [Defining an action](#defining),
96
96
  [Run context](#run-context), [Access control](#access-control),
@@ -483,7 +483,7 @@ await runAgentLoop({
483
483
 
484
484
  For most apps, scheduled prompts and integration webhooks already call this loop
485
485
  for you. Reach for it directly only when building a custom no-browser host, eval
486
- runner, or server-side orchestration surface. See [Server: Production agent handler](/docs/server#agent-handler) for the full signature.
486
+ runner, or server-side orchestration surface. See [Server: Production agent handler](/docs/server-plugins#agent-chat-plugin) for the full signature.
487
487
 
488
488
  ### Running against a folder {#folder-loop}
489
489
 
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: "Agents"
3
+ description: "Agent configuration, instructions, and the surfaces agents operate through."
4
+ draft: true
5
+ ---
6
+
7
+ # Agents
8
+
9
+ This section is coming soon. In the meantime, see [Writing Agent Instructions](/docs/writing-agent-instructions), [Skills](/docs/skills-guide), and [Agent Surfaces](/docs/agent-surfaces) for existing coverage.
@@ -123,7 +123,7 @@ import * as schema from "./schema.js";
123
123
  export const getDb = createGetDb(schema);
124
124
  ```
125
125
 
126
- Import `getDb` from this template-local path — `../../server/db/index.js` in routes, `../server/db/index.js` in actions — rather than from `@agent-native/core` directly. The core export returns a generic untyped instance; the template's `getDb()` carries your schema types. See [Server](/docs/server#request-context) for how actions and custom routes each import it.
126
+ Import `getDb` from this template-local path — `../../server/db/index.js` in routes, `../server/db/index.js` in actions — rather than from `@agent-native/core` directly. The core export returns a generic untyped instance; the template's `getDb()` carries your schema types. See [Server](/docs/server-routes#request-context) for how actions and custom routes each import it.
127
127
 
128
128
  ## Dialect-Agnostic Schema And Queries {#schema}
129
129
 
@@ -307,6 +307,6 @@ Instead of pushing directly, schema changes should be applied via SQL migrations
307
307
 
308
308
  - [**Security — Data Scoping**](/docs/security#data-scoping) — how `owner_email` and access helpers scope reads and writes
309
309
  - [**Sharing**](/docs/sharing#building) — `ownableColumns()` and the visibility model for shared resources
310
- - [**Server**](/docs/server#request-context) — how actions and custom routes each import `getDb`
310
+ - [**Server**](/docs/server-routes#request-context) — how actions and custom routes each import `getDb`
311
311
  - [**Deployment**](/docs/deployment#persistent-database) — connecting a persistent database per deploy target
312
312
  - [**Actions**](/docs/actions#access-control) — a complete, paste-ready action that reads and writes through `getDb`/`schema`
@@ -15,7 +15,7 @@ You don't need to build agent-native from scratch. The agent chat, resources tab
15
15
 
16
16
  <Callout tone="info">
17
17
 
18
- **Prerequisite:** the server has to be running the `agent-chat-plugin` (it auto-mounts in every template). If you're starting from scratch, see [Server](/docs/server). Need the public API map instead of a tutorial? See [Component API](/docs/components).
18
+ **Prerequisite:** the server has to be running the `agent-chat-plugin` (it auto-mounts in every template). If you're starting from scratch, see [Server](/docs/server-overview). Need the public API map instead of a tutorial? See [Component API](/docs/components).
19
19
 
20
20
  </Callout>
21
21
 
@@ -374,4 +374,4 @@ Automations can chain off this — e.g. _"if a critical notification fires, also
374
374
 
375
375
  - [**Automations**](/docs/automations) — the most common caller of `notify()`
376
376
  - [**Security**](/docs/security) — the `${keys.NAME}` substitution that powers the webhook channel
377
- - [**Server plugins**](/docs/server) — where custom channels are registered at startup
377
+ - [**Server plugins**](/docs/server-plugins) — where custom channels are registered at startup
@@ -113,5 +113,5 @@ navigate(`/inbox/${threadId}`);
113
113
  ## What's next
114
114
 
115
115
  - [**Client**](/docs/client) — the agent-native browser hooks and utilities
116
- - [**Server**](/docs/server) — file-based server routes and the `/_agent-native/` namespace
116
+ - [**Server**](/docs/server-overview) — file-based server routes and the `/_agent-native/` namespace
117
117
  - [**Context Awareness**](/docs/context-awareness) — how the agent learns the current route and selection
@@ -0,0 +1,437 @@
1
+ ---
2
+ title: "Database"
3
+ description: "Connect a portable SQL database to your agent-native app: schema helpers, migrations, and the hosted database options."
4
+ ---
5
+
6
+ # Database
7
+
8
+ Every agent-native app stores its state in SQL. The UI and the agent both read and write the same tables through the same actions, using the same [Drizzle ORM](https://orm.drizzle.team) client. When either side changes data, the other sees it through the polling layer.
9
+
10
+ <Diagram id="doc-block-9mtx2f" title="How data flows to the database" summary="The UI and the agent both reach the database through the same Actions layer. Both callers use the same getDb() client and the same SQL tables. There is no separate backend for each.">
11
+
12
+ ```html
13
+ <div class="diagram-dbr">
14
+ <div class="diagram-col">
15
+ <div class="diagram-node">Browser / UI</div>
16
+ <div class="diagram-node">Agent loop</div>
17
+ </div>
18
+ <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
19
+ <div class="diagram-panel" data-rough>
20
+ <strong>Actions</strong>
21
+ <small class="diagram-muted">defineAction</small>
22
+ <small class="diagram-muted">getDb() &middot; Drizzle ORM</small>
23
+ </div>
24
+ <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
25
+ <div class="diagram-box" data-rough>
26
+ SQL database<br /><small class="diagram-muted"
27
+ >dialect from DATABASE_URL</small
28
+ >
29
+ </div>
30
+ </div>
31
+ ```
32
+
33
+ ```css
34
+ .diagram-dbr {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 14px;
38
+ flex-wrap: wrap;
39
+ }
40
+ .diagram-dbr .diagram-col {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 10px;
44
+ }
45
+ .diagram-dbr .diagram-panel {
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: 4px;
49
+ padding: 12px 16px;
50
+ }
51
+ .diagram-dbr .diagram-arrow {
52
+ font-size: 22px;
53
+ line-height: 1;
54
+ }
55
+ ```
56
+
57
+ </Diagram>
58
+
59
+ ## Hosting Options {#hosting}
60
+
61
+ The app detects which backend to use from `DATABASE_URL`. When the variable is unset, it falls back to a local SQLite file so you can start without any setup.
62
+
63
+ ### Default: SQLite file {#default-sqlite}
64
+
65
+ When `DATABASE_URL` is not set, the app creates a SQLite database at `data/app.db`. No configuration required. Start the dev server and the file is created on first run.
66
+
67
+ This is for local development only. Containers, serverless functions, and preview environments may reset their filesystem between restarts, so a local SQLite file can disappear. Set `DATABASE_URL` to a persistent hosted database before deploying.
68
+
69
+ ### Local Postgres: PGlite {#local-pglite}
70
+
71
+ To develop locally against the Postgres dialect without Docker or a hosted database, install the optional PGlite package and set `DATABASE_URL`:
72
+
73
+ ```bash
74
+ pnpm add @electric-sql/pglite@^0.5.3
75
+ ```
76
+
77
+ ```bash
78
+ DATABASE_URL=pglite:./data/pglite
79
+ ```
80
+
81
+ [PGlite](https://pglite.dev/) runs an in-process WASM Postgres database. It lets you catch Postgres-only schema issues before deploying, while keeping setup as simple as the SQLite default. Like SQLite, it is local-only storage. Do not use it for production or shared environments.
82
+
83
+ ### Production database {#production}
84
+
85
+ Set `DATABASE_URL` in your `.env` file or deploy-provider environment to connect a hosted database:
86
+
87
+ ```bash
88
+ # Neon Postgres
89
+ DATABASE_URL=postgres://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/mydb?sslmode=require
90
+
91
+ # Supabase Postgres
92
+ DATABASE_URL=postgres://postgres.xxxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres
93
+
94
+ # Plain Postgres
95
+ DATABASE_URL=postgres://user:pass@localhost:5432/mydb
96
+
97
+ # Turso (libSQL). Also requires DATABASE_AUTH_TOKEN.
98
+ DATABASE_URL=libsql://my-db-org.turso.io
99
+ ```
100
+
101
+ The framework auto-detects the dialect from the URL prefix and configures Drizzle accordingly.
102
+
103
+ ### Builder.io managed database {#builder-managed}
104
+
105
+ _Planned (not yet available):_ when connected to Builder.io, your app will be able to use a managed database provisioned automatically, with no connection strings required.
106
+
107
+ ## Setting Up the Database {#setup}
108
+
109
+ An app that uses the database needs three files:
110
+
111
+ - `server/db/schema.ts`: table definitions
112
+ - `server/db/index.ts`: the typed DB client singleton
113
+ - `server/plugins/db.ts`: migrations that run at startup
114
+
115
+ ### 1. Define your schema {#schema}
116
+
117
+ Import schema helpers from `@agent-native/core/db/schema`. Never import from `drizzle-orm/sqlite-core` or `drizzle-orm/pg-core` directly. The framework helpers produce dialect-agnostic definitions that work across all supported backends.
118
+
119
+ ```ts filename="server/db/schema.ts"
120
+ import { integer, now, table, text } from "@agent-native/core/db/schema";
121
+
122
+ export const tasks = table("tasks", {
123
+ id: text("id").primaryKey(),
124
+ title: text("title").notNull(),
125
+ priority: integer("priority").notNull().default(0),
126
+ done: integer("done", { mode: "boolean" }).notNull().default(false),
127
+ ownerEmail: text("owner_email").notNull(),
128
+ createdAt: text("created_at").notNull().default(now()),
129
+ });
130
+ ```
131
+
132
+ | Helper | Purpose |
133
+ | --------- | ---------------------------------------------------------------------- |
134
+ | `table` | Define a table; dispatches to `pgTable` or `sqliteTable` |
135
+ | `text` | Text column, supports `{ enum: [...] }` |
136
+ | `integer` | Integer column, `{ mode: "boolean" }` maps to Postgres boolean |
137
+ | `real` | Float column. Maps to `real` on SQLite, `double precision` on Postgres |
138
+ | `now` | Dialect-agnostic current timestamp for `.default(now())` |
139
+
140
+ <DataModel
141
+ id="doc-block-3hbven"
142
+ title="The tasks table"
143
+ summary={
144
+ "Defined once with the framework helpers; the dialect is chosen at runtime from DATABASE_URL."
145
+ }
146
+ entities={[
147
+ {
148
+ id: "tasks",
149
+ name: "tasks",
150
+ note: "Domain table. Add owner_email (or ...ownableColumns()) so SQL-level scoping can filter rows to the authenticated user.",
151
+ fields: [
152
+ {
153
+ name: "id",
154
+ type: "text",
155
+ pk: true,
156
+ nullable: false,
157
+ },
158
+ {
159
+ name: "title",
160
+ type: "text",
161
+ nullable: false,
162
+ },
163
+ {
164
+ name: "priority",
165
+ type: "integer",
166
+ nullable: false,
167
+ note: "default 0",
168
+ },
169
+ {
170
+ name: "done",
171
+ type: "integer (boolean mode)",
172
+ nullable: false,
173
+ note: "default false; maps to a Postgres boolean",
174
+ },
175
+ {
176
+ name: "owner_email",
177
+ type: "text",
178
+ nullable: false,
179
+ note: "enables data scoping",
180
+ },
181
+ {
182
+ name: "created_at",
183
+ type: "text",
184
+ nullable: false,
185
+ note: "default now()",
186
+ },
187
+ ],
188
+ },
189
+ ]}
190
+ />
191
+
192
+ Tables that store per-user data must include an `owner_email` column so the framework can filter rows to the authenticated user. Tables that also support sharing with other users or orgs should spread `...ownableColumns()` instead, which adds `owner_email`, `org_id`, and `visibility` in one call. See [Scoping Data to Users](#scoping) below.
193
+
194
+ ### 2. Create the DB client {#db-client}
195
+
196
+ Each app creates a lazy, singleton Drizzle client by calling `createGetDb(schema)`. The canonical location is `server/db/index.ts`:
197
+
198
+ ```ts filename="server/db/index.ts"
199
+ import { createGetDb } from "@agent-native/core/db";
200
+ import * as schema from "./schema.js";
201
+
202
+ export const getDb = createGetDb(schema);
203
+ ```
204
+
205
+ `createGetDb` returns a `getDb()` function that opens the database connection on first call and returns the same typed Drizzle instance on subsequent calls. It reads `DATABASE_URL` at runtime to determine which backend and dialect to use.
206
+
207
+ Import `getDb` from this template-local path in actions and routes. Do not import from `@agent-native/core` directly. The core export is untyped; the local export carries your schema types.
208
+
209
+ ### 3. Write migrations {#migrations}
210
+
211
+ Schema changes are applied at startup via a Nitro plugin in `server/plugins/db.ts`. Use `runMigrations` from `@agent-native/core/db`:
212
+
213
+ <AnnotatedCode
214
+ id="doc-block-7qwlpz"
215
+ title="A startup migration plugin"
216
+ filename="server/plugins/db.ts"
217
+ language="ts"
218
+ code={
219
+ 'import { runMigrations } from "@agent-native/core/db";\n\nexport default runMigrations(\n [\n {\n version: 1,\n sql: `CREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n done INTEGER NOT NULL DEFAULT 0,\n owner_email TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP\n )`,\n },\n {\n version: 2,\n sql: `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 0`,\n },\n {\n // Dialect-gated: runs only on the matching backend.\n version: 3,\n sql: {\n postgres: `ALTER TABLE tasks ADD COLUMN IF NOT EXISTS tsv tsvector`,\n sqlite: `SELECT 1`,\n },\n },\n ],\n { table: "my_app_migrations" },\n);'
220
+ }
221
+ annotations={[
222
+ {
223
+ lines: "6",
224
+ label: "Per-app version table",
225
+ note: "Each app tracks its own applied versions in a separate table. Use a name unique to your app so it doesn't collide with framework migrations or other apps sharing the same database.",
226
+ },
227
+ {
228
+ lines: "7",
229
+ label: "Initial table creation",
230
+ note: "`CREATE TABLE IF NOT EXISTS` is safe to re-run on every restart. Put the full table definition in version 1.",
231
+ },
232
+ {
233
+ lines: "17",
234
+ label: "Additive column adds",
235
+ note: "`ADD COLUMN IF NOT EXISTS` is the safe way to add columns after initial creation. Never drop or rename columns.",
236
+ },
237
+ {
238
+ lines: "22-25",
239
+ label: "Dialect gating",
240
+ note: "Pass an object keyed by dialect to run different SQL per backend. Use `SELECT 1` as a no-op on the dialect that doesn't need the change.",
241
+ },
242
+ ]}
243
+ />
244
+
245
+ `runMigrations` runs each entry in order once, then records the version so it is skipped on future restarts. Entries are idempotent. The plugin is safe to call on every boot.
246
+
247
+ <Callout tone="warning">
248
+ Give every new migration a stable `name` (e.g. `{ version: 4, name:
249
+ "tasks-priority-index", sql: "..." }`). The plain `version` number is only a
250
+ position in a shared sequence: if two branches each add their own
251
+ migrations at, say, `version: 4`, whichever branch deploys first "uses up"
252
+ that version number in the bookkeeping table, and the other branch's DDL
253
+ silently never runs when it merges — the table looks fully migrated even
254
+ though the second branch's columns or tables don't exist. A `name` is
255
+ tracked independently of `version`, so it applies exactly once regardless
256
+ of what version number it shipped under or which branch merged first.
257
+ Unnamed legacy migrations keep working as-is; add `name` to migrations you
258
+ write from now on.
259
+ </Callout>
260
+
261
+ <Callout tone="risk">
262
+ Never run `drizzle-kit push` against a production database. Template schemas
263
+ only define app-specific tables; they do not include central framework tables
264
+ (`user`, `session`, `application_state`, and others). Running `drizzle-kit
265
+ push` against production will detect those tables as unknown and attempt to
266
+ drop them, causing immediate data loss.
267
+ </Callout>
268
+
269
+ `drizzle.config.ts` at the root of each app configures drizzle-kit for local development schema inspection:
270
+
271
+ ```ts filename="drizzle.config.ts"
272
+ import { createDrizzleConfig } from "@agent-native/core/db/drizzle-config";
273
+ export default createDrizzleConfig();
274
+ ```
275
+
276
+ Use `pnpm db:generate` to inspect your schema and `pnpm db:push` against a local database only.
277
+
278
+ ### 4. Query in actions {#querying}
279
+
280
+ Call `getDb()` from your actions to get the typed Drizzle client. Use Drizzle's query builder and portable operators from `drizzle-orm`:
281
+
282
+ ```ts
283
+ import { and, desc, eq } from "drizzle-orm";
284
+ import { getDb } from "../server/db/index.js";
285
+ import * as schema from "../server/db/schema.js";
286
+
287
+ const db = getDb();
288
+
289
+ const openTasks = await db
290
+ .select()
291
+ .from(schema.tasks)
292
+ .where(
293
+ and(eq(schema.tasks.ownerEmail, userEmail), eq(schema.tasks.done, false)),
294
+ )
295
+ .orderBy(desc(schema.tasks.createdAt));
296
+
297
+ await db
298
+ .update(schema.tasks)
299
+ .set({ done: true })
300
+ .where(eq(schema.tasks.id, taskId));
301
+ ```
302
+
303
+ ## Scoping Data to Users {#scoping}
304
+
305
+ All reads and writes against user-facing tables must be scoped to the authenticated user. The framework provides two patterns depending on whether the data is private or shareable.
306
+
307
+ **Private data:** tables that belong to one user. Add `ownerEmail: text("owner_email").notNull()` to the schema and include `eq(table.ownerEmail, userEmail)` in every query:
308
+
309
+ ```ts
310
+ .where(eq(schema.tasks.ownerEmail, userEmail))
311
+ ```
312
+
313
+ **Shared resources:** tables that can be shared with other users or organizations. Spread `...ownableColumns()` in the schema instead of a bare `owner_email`. This adds `owner_email`, `org_id`, and `visibility` in one call, and creates a companion shares table with `createSharesTable`:
314
+
315
+ ```ts filename="server/db/schema.ts"
316
+ import {
317
+ table,
318
+ text,
319
+ ownableColumns,
320
+ createSharesTable,
321
+ } from "@agent-native/core/db/schema";
322
+
323
+ export const decks = table("decks", {
324
+ id: text("id").primaryKey(),
325
+ title: text("title").notNull(),
326
+ ...ownableColumns(),
327
+ });
328
+ export const deckShares = createSharesTable("deck_shares");
329
+ ```
330
+
331
+ Then use `accessFilter` from `@agent-native/core/sharing` in list queries instead of a manual `eq` check:
332
+
333
+ ```ts
334
+ import { accessFilter } from "@agent-native/core/sharing";
335
+
336
+ const rows = await db
337
+ .select()
338
+ .from(schema.decks)
339
+ .where(accessFilter(schema.decks, schema.deckShares));
340
+ ```
341
+
342
+ `accessFilter` builds a query that admits rows the caller owns, rows shared with their org, and rows explicitly shared with them. It does not expose rows from other users.
343
+
344
+ See [Security — Data Scoping](/docs/security#data-scoping) and [Sharing](/docs/sharing#building) for the full model.
345
+
346
+ ## SQL-Backed Sync {#sync}
347
+
348
+ Agent-native does not rely on filesystem watchers or sticky in-memory state. When an action writes to the database, a sync version increments. The client `useDbSync()` hook polls `/_agent-native/poll` and invalidates React Query caches when it sees a higher version.
349
+
350
+ This works across serverless and multi-instance deployments because the database is the coordination point. If you write custom mutations outside actions, use framework helpers or emit the appropriate sync invalidation so open UIs refresh.
351
+
352
+ <Diagram id="doc-block-qhkdv8" title="SQL-backed sync loop" summary="No watchers, no sticky state. A write bumps a version in SQL; every client polls the version and refetches.">
353
+
354
+ ```html
355
+ <div class="diagram-db-sync">
356
+ <div class="diagram-box" data-rough>
357
+ Action / helper<br /><small class="diagram-muted">mutates data</small>
358
+ </div>
359
+ <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
360
+ <div class="diagram-panel" data-rough>
361
+ <strong>SQL database</strong
362
+ ><small class="diagram-muted">sync version increments</small>
363
+ </div>
364
+ <div class="diagram-arrow diagram-muted" aria-hidden="true">&larr;</div>
365
+ <div class="diagram-col">
366
+ <div class="diagram-node">
367
+ useDbSync()<br /><small class="diagram-muted"
368
+ >polls /_agent-native/poll</small
369
+ >
370
+ </div>
371
+ <div class="diagram-pill ok">invalidate caches &rarr; UI refreshes</div>
372
+ </div>
373
+ </div>
374
+ ```
375
+
376
+ ```css
377
+ .diagram-db-sync {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 14px;
381
+ flex-wrap: wrap;
382
+ }
383
+ .diagram-db-sync .diagram-col {
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 8px;
387
+ align-items: flex-start;
388
+ }
389
+ .diagram-db-sync .diagram-arrow {
390
+ font-size: 22px;
391
+ line-height: 1;
392
+ }
393
+ ```
394
+
395
+ </Diagram>
396
+
397
+ <Endpoint id="doc-block-1s11d2e" title="The poll endpoint" method="GET" path="/_agent-native/poll" summary="Return the current per-source database sync versions so the client can detect changes." auth="Session cookie (request-scoped identity)" responses={[
398
+ {
399
+ "status": "200",
400
+ "description": "Current sync versions keyed by source."
401
+ }
402
+ ]}>
403
+
404
+ `useDbSync()` calls this on an interval and falls back to it when SSE is unavailable. When a returned version is higher than the client's last-seen value, the matching React Query caches are invalidated and refetch.
405
+
406
+ </Endpoint>
407
+
408
+ ## Raw SQL {#raw-sql}
409
+
410
+ For advanced queries, health checks, or one-off maintenance that the Drizzle query builder can't express, use `getDbExec` from `@agent-native/core/db`:
411
+
412
+ ```ts
413
+ import { getDbExec, isPostgres, intType } from "@agent-native/core/db";
414
+
415
+ const { rows } = await getDbExec().execute({
416
+ sql: `SELECT id, title FROM tasks WHERE owner_email = ? LIMIT ?`,
417
+ args: [userEmail, 50],
418
+ });
419
+ ```
420
+
421
+ `getDbExec` auto-converts `?` params to `$1`, `$2`, etc. for Postgres. Use `isPostgres()` to branch on dialect, and `intType()` to return the correct integer type for the current backend. Prefer the Drizzle query builder for normal reads and writes. Raw SQL bypasses type safety and is harder to maintain.
422
+
423
+ ## Environment Variables {#environment-variables}
424
+
425
+ | Variable | Purpose |
426
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
427
+ | `DATABASE_URL` | Persistent SQL connection string (unset = local SQLite; `pglite:./data/pglite` = local Postgres opt-in) |
428
+ | `DATABASE_AUTH_TOKEN` | Auth token for providers that require a separate token, such as Turso/libSQL |
429
+
430
+ ## What's next {#whats-next}
431
+
432
+ - [**Security — Data Scoping**](/docs/security#data-scoping): how `owner_email` and access helpers scope reads and writes
433
+ - [**Sharing**](/docs/sharing#building): `ownableColumns()` and the visibility model for shared resources
434
+ - [**Plugins**](/docs/server-plugins): the startup plugin lifecycle where migrations run
435
+ - [**Actions**](/docs/actions): the surface where actions call `getDb()` to read and write data
436
+ - [**Deployment**](/docs/deployment#persistent-database): connecting a persistent database per deploy target
437
+ - [**Real-Time Sync**](/docs/key-concepts#polling-sync): `useDbSync()` and the full client-side sync model