@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.
@@ -0,0 +1,194 @@
1
+ ---
2
+ title: "Routes"
3
+ description: "File-based Nitro server routes, the SSR catch-all, naming conventions, and scoping custom routes to the authenticated user."
4
+ ---
5
+
6
+ # Routes
7
+
8
+ Server routing in an agent-native app is split across two surfaces.
9
+
10
+ **Framework routes** at `/_agent-native/*` are managed by the framework. They expose the agent chat loop, sync polling, action endpoints, and other core capabilities. You do not create or modify these routes.
11
+
12
+ **Custom file routes** in `server/routes/` are where you add your own endpoints. Nitro picks them up automatically based on where they sit in the directory tree — no registration or config needed. The filename sets the HTTP method, the directory path becomes the URL path, and each file exports a single handler.
13
+
14
+ ## Framework-Mounted Routes {#framework-routes}
15
+
16
+ The framework reserves the `/_agent-native/` namespace for its own endpoints. Never put custom routes here. This table is representative, not exhaustive.
17
+
18
+ | Route prefix | Purpose |
19
+ | -------------------------------- | ------------------------------------------------------------------------------- |
20
+ | `/_agent-native/actions/:name` | Action HTTP endpoints |
21
+ | `/_agent-native/agent-chat` | Agent chat loop |
22
+ | `/_agent-native/poll` | SQL-backed UI sync |
23
+ | `/_agent-native/runs` | Progress primitive. The `RunsTray` polls this; see [Progress](/docs/progress) |
24
+ | `/_agent-native/resources/*` | Workspace resources |
25
+ | `/_agent-native/extensions/*` | Runtime extensions and extension proxy (legacy alias: `/_agent-native/tools/*`) |
26
+ | `/_agent-native/integrations/*` | Messaging/webhook integrations |
27
+ | `/_agent-native/a2a` | Agent-to-agent JSON-RPC |
28
+ | `/mcp` | MCP endpoint |
29
+ | `/_agent-native/onboarding/*` | Setup checklist |
30
+ | `/_agent-native/observability/*` | Traces, feedback, evals, experiments |
31
+ | `/_agent-native/file-upload` | File upload provider endpoint |
32
+
33
+ Custom app routes go in `/api/*` or another path that doesn't collide with `/_agent-native/`.
34
+
35
+ ## When to Use a Custom Route {#when-custom-routes}
36
+
37
+ Actions cover the vast majority of app operations. Reach for a file route in `server/routes/` only when you need something an action can't express.
38
+
39
+ <Comparison>
40
+
41
+ ### Use a custom route {#use-custom-route}
42
+
43
+ - File uploads or multipart form data
44
+ - Streaming responses or server-sent events (SSE)
45
+ - Inbound webhooks that need raw body verification
46
+ - OAuth callbacks
47
+ - Public unauthenticated pages
48
+ - External REST contracts that require a specific URL path or response shape
49
+
50
+ ### Use an action instead {#use-action-instead}
51
+
52
+ - Operations the UI and agent both need
53
+ - Reading or writing app data
54
+ - HTTP-shaped endpoints (use the `http` option in [Actions](/docs/actions))
55
+ - Anything that should be an agent tool, MCP tool, or CLI command
56
+ - Standard JSON request/response APIs
57
+
58
+ </Comparison>
59
+
60
+ ### Example: Inbound webhook {#example-webhook}
61
+
62
+ Webhooks are a clear case for a custom route. External services send a raw HTTP body that must be read as bytes before parsing, so you can verify the request signature. Actions receive pre-parsed JSON and cannot access the raw body, which makes signature verification impossible inside one.
63
+
64
+ The route below handles an inbound webhook at `POST /api/webhooks/events`. It reads the raw body, verifies an HMAC signature, then processes the payload.
65
+
66
+ ```ts filename="server/routes/api/webhooks/events.post.ts"
67
+ import { createHmac, timingSafeEqual } from "crypto";
68
+ import { createError, defineEventHandler, getHeader, readRawBody } from "h3";
69
+
70
+ export default defineEventHandler(async (event) => {
71
+ const secret = process.env.WEBHOOK_SECRET;
72
+ const signature = getHeader(event, "x-webhook-signature");
73
+ const rawBody = await readRawBody(event);
74
+
75
+ if (!secret || !signature || !rawBody) {
76
+ throw createError({ statusCode: 400, statusMessage: "Bad Request" });
77
+ }
78
+
79
+ const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
80
+ if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
81
+ throw createError({ statusCode: 401, statusMessage: "Invalid signature" });
82
+ }
83
+
84
+ const payload = JSON.parse(rawBody) as { type: string; data: unknown };
85
+ // handle payload.type ...
86
+
87
+ return { received: true };
88
+ });
89
+ ```
90
+
91
+ Nitro maps this file to `POST /api/webhooks/events` automatically. The `WEBHOOK_SECRET` environment variable should be set to the signing secret provided by the external service.
92
+
93
+ ## File-Based Routes {#file-based-routes}
94
+
95
+ The directory path becomes the URL path, and the filename suffix sets the HTTP method. A file at `server/routes/api/items/[id].get.ts` maps to `GET /api/items/:id` — `items/` comes from the directory, `[id]` becomes the `:id` param, and `.get` sets the method.
96
+
97
+ ```text
98
+ server/routes/
99
+ api/
100
+ items/
101
+ index.get.ts -> GET /api/items
102
+ index.post.ts -> POST /api/items
103
+ [id].get.ts -> GET /api/items/:id
104
+ [id].patch.ts -> PATCH /api/items/:id
105
+ [id].delete.ts -> DELETE /api/items/:id
106
+ webhooks/
107
+ stripe.post.ts -> POST /api/webhooks/stripe
108
+ [...page].get.ts -> SSR catch-all for public pages
109
+ ```
110
+
111
+ `defineEventHandler` is the H3 primitive for writing a route. It wraps your handler function and gives it access to the `event` object, which carries the request, response, router params, query string, headers, and body. Returning a value from the handler sends it as the response — a plain object becomes JSON automatically. Throwing a `createError` sends the appropriate HTTP error status.
112
+
113
+ Router params like `[id]` are read from `event` with `getRouterParam`:
114
+
115
+ ```ts filename="server/routes/api/items/[id].get.ts"
116
+ import { defineEventHandler, getRouterParam, createError } from "h3";
117
+
118
+ export default defineEventHandler((event) => {
119
+ const id = getRouterParam(event, "id");
120
+ if (!id) throw createError({ statusCode: 400, statusMessage: "Missing id" });
121
+ // fetch and return the item by id
122
+ });
123
+ ```
124
+
125
+ ### Naming conventions {#naming-conventions}
126
+
127
+ | File path (relative to `server/routes/`) | HTTP method | URL path |
128
+ | ---------------------------------------- | ----------- | ---------------- |
129
+ | `api/items/index.get.ts` | GET | `/api/items` |
130
+ | `api/items/index.post.ts` | POST | `/api/items` |
131
+ | `api/items/[id].get.ts` | GET | `/api/items/:id` |
132
+ | `api/items/[id].patch.ts` | PATCH | `/api/items/:id` |
133
+ | `api/items/[id].delete.ts` | DELETE | `/api/items/:id` |
134
+ | `api/items/[...slug].get.ts` | GET | `/api/items/*` |
135
+
136
+ ## Request Context and Access {#request-context}
137
+
138
+ Actions mounted by the framework automatically run with request context. Custom routes do not. If a custom route reads or writes ownable resources, load the session and wrap the work:
139
+
140
+ <AnnotatedCode
141
+ id="doc-block-server-routes-ctx"
142
+ title="Scoping a custom route to the request user"
143
+ filename="server/routes/api/projects.get.ts"
144
+ language="ts"
145
+ code={
146
+ 'import { defineEventHandler, createError } from "h3";\nimport { getSession, runWithRequestContext } from "@agent-native/core/server";\nimport { getDb } from "../../db/index.js";\nimport { accessFilter } from "@agent-native/core/sharing";\nimport * as schema from "../../db/schema";\n\nexport default defineEventHandler(async (event) => {\n const session = await getSession(event);\n if (!session?.email) {\n throw createError({ statusCode: 401, statusMessage: "Unauthorized" });\n }\n\n return runWithRequestContext(\n { userEmail: session.email, orgId: session.orgId },\n async () => {\n const db = getDb();\n return db\n .select()\n .from(schema.projects)\n .where(accessFilter(schema.projects, schema.projectShares));\n },\n );\n});'
147
+ }
148
+ annotations={[
149
+ {
150
+ lines: "8-11",
151
+ label: "Custom routes have no auto-context",
152
+ note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.",
153
+ },
154
+ {
155
+ lines: "13",
156
+ label: "Establish request context",
157
+ note: "`runWithRequestContext` makes the user/org available to scoping helpers for the duration of the work.",
158
+ },
159
+ {
160
+ lines: "17-20",
161
+ label: "Scope ownable reads",
162
+ note: "`accessFilter` constrains the query to rows the caller may see. Never run an unscoped `db.select().from(ownableTable)` here.",
163
+ },
164
+ ]}
165
+ />
166
+
167
+ `getDb` is created per app via `createGetDb(schema)` in `server/db/index.ts`, so custom routes import it from the template path (`../../db/index.js`), not from `@agent-native/core/db`. See [Database](/docs/server-database#db-client).
168
+
169
+ <Callout tone="warning">
170
+ Do not run unscoped `db.select().from(ownableTable)` in custom routes. Always
171
+ establish request context with `runWithRequestContext` before querying ownable
172
+ data.
173
+ </Callout>
174
+
175
+ ## SSR Catch-All {#ssr-catch-all}
176
+
177
+ The `[...page].get.ts` route at the root of `server/routes/` handles server-side rendering for public-facing pages. It uses `createH3SSRHandler` to hand off to the React Router build:
178
+
179
+ ```ts filename="server/routes/[...page].get.ts"
180
+ import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
181
+
182
+ export default createH3SSRHandler(
183
+ () => import("virtual:react-router/server-build"),
184
+ );
185
+ ```
186
+
187
+ Every SSR response is an impersonal, public shell cached at the CDN. Never read cookies, session state, or auth branches in this handler. Personalization happens client-side after hydration.
188
+
189
+ ## What's next {#whats-next}
190
+
191
+ - [**Actions**](/docs/actions): the preferred surface for app operations; reach for a route only when it doesn't fit
192
+ - [**Plugins**](/docs/server-plugins): startup plugins that run before routes are served
193
+ - [**Middleware**](/docs/server-middleware): auth guards that protect all routes including `/api/*`
194
+ - [**Security**](/docs/security#access-guards): `accessFilter`/`assertAccess` and the full data-scoping model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.141.4",
3
+ "version": "0.141.5",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1,469 +0,0 @@
1
- ---
2
- title: "Server"
3
- description: "Nitro server routes, plugins, framework-mounted routes, request context, and SQL-backed sync."
4
- ---
5
-
6
- # Server
7
-
8
- Agent-native apps use [Nitro](https://nitro.build) for server routes and plugins. Most product behavior should live in [Actions](/docs/actions); custom routes are for protocol surfaces that actions do not fit: uploads, streaming, public pages, webhooks, OAuth callbacks, and provider-specific APIs.
9
-
10
- <Diagram id="doc-block-1cve8m0" title="What runs on the server" summary="Actions are the default. Custom file routes and framework-mounted routes share the same Nitro app and the same SQL database.">
11
-
12
- ```html
13
- <div class="diagram-server">
14
- <div class="diagram-col entry">
15
- <div class="diagram-node">Browser / UI</div>
16
- <div class="diagram-node">Agent loop</div>
17
- <div class="diagram-node">
18
- External clients<br /><small class="diagram-muted"
19
- >HTTP · MCP · A2A</small
20
- >
21
- </div>
22
- </div>
23
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
24
- <div class="diagram-panel" data-rough>
25
- <strong>Nitro server</strong>
26
- <div class="diagram-row">
27
- <span class="diagram-pill accent">Actions</span
28
- ><small class="diagram-muted">default surface</small>
29
- </div>
30
- <div class="diagram-row">
31
- <span class="diagram-pill">/_agent-native/*</span
32
- ><small class="diagram-muted">framework routes</small>
33
- </div>
34
- <div class="diagram-row">
35
- <span class="diagram-pill">/api/*</span
36
- ><small class="diagram-muted">custom file routes</small>
37
- </div>
38
- <div class="diagram-row">
39
- <span class="diagram-pill">plugins</span
40
- ><small class="diagram-muted">startup: migrations, jobs</small>
41
- </div>
42
- </div>
43
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
44
- <div class="diagram-box" data-rough>
45
- SQL database<br /><small class="diagram-muted"
46
- >Drizzle · the coordination point</small
47
- >
48
- </div>
49
- </div>
50
- ```
51
-
52
- ```css
53
- .diagram-server {
54
- display: flex;
55
- align-items: center;
56
- gap: 14px;
57
- flex-wrap: wrap;
58
- }
59
- .diagram-server .diagram-col {
60
- display: flex;
61
- flex-direction: column;
62
- gap: 10px;
63
- }
64
- .diagram-server .diagram-panel {
65
- display: flex;
66
- flex-direction: column;
67
- gap: 8px;
68
- padding: 14px 16px;
69
- }
70
- .diagram-server .diagram-row {
71
- display: flex;
72
- align-items: center;
73
- gap: 8px;
74
- }
75
- .diagram-server .diagram-arrow {
76
- font-size: 22px;
77
- line-height: 1;
78
- }
79
- ```
80
-
81
- </Diagram>
82
-
83
- ## File-Based Routes {#file-based-routes}
84
-
85
- Routes live in `server/routes/` and Nitro maps filenames to methods and paths:
86
-
87
- ```text
88
- server/routes/
89
- api/
90
- health.get.ts -> GET /api/health
91
- uploads.post.ts -> POST /api/uploads
92
- webhooks/
93
- stripe.post.ts -> POST /api/webhooks/stripe
94
- [...page].get.ts -> SSR catch-all for public pages
95
- ```
96
-
97
- Each route exports a `defineEventHandler`:
98
-
99
- ```ts filename="server/routes/api/health.get.ts"
100
- import { defineEventHandler } from "h3";
101
-
102
- export default defineEventHandler(() => ({
103
- ok: true,
104
- service: "my-template",
105
- }));
106
- ```
107
-
108
- ### Route naming conventions {#route-naming-conventions}
109
-
110
- | File name pattern | HTTP method | Example path |
111
- | ------------------ | ----------- | --------------------------- |
112
- | `index.get.ts` | GET | `/api/items` |
113
- | `index.post.ts` | POST | `/api/items` |
114
- | `[id].get.ts` | GET | `/api/items/:id` |
115
- | `[id].patch.ts` | PATCH | `/api/items/:id` |
116
- | `[id].delete.ts` | DELETE | `/api/items/:id` |
117
- | `[...slug].get.ts` | GET | `/api/items/*` or catch-all |
118
-
119
- ## Prefer Actions For App Operations {#actions-first}
120
-
121
- If the UI and agent both need to do something, define an action instead of a custom API route. Actions automatically become:
122
-
123
- - Agent tools.
124
- - Typed frontend hooks.
125
- - HTTP endpoints under `/_agent-native/actions/:name`.
126
- - MCP and A2A-callable tools.
127
- - CLI commands for development.
128
-
129
- Use custom `/api/*` routes only when you need a route-shaped protocol or binary/streaming behavior. See [Actions](/docs/actions).
130
-
131
- ### API-Only Apps {#api-only-apps}
132
-
133
- You do not need an Express server just because the app is API-only. An agent-native app already ships with a Nitro/H3 server, and `defineAction()` is the default API surface. Define operations in `actions/`; the framework mounts each action at `/_agent-native/actions/<name>` with schema validation and request context.
134
-
135
- Use the action `http` option to shape direct HTTP access: actions are `POST` by default, `http: { method: "GET" | "PUT" | "DELETE" }` changes the verb, and `http: false` keeps an action off HTTP. Reach for `server/routes/api/*` only for route-shaped or protocol concerns: file uploads, streaming/SSE, webhooks, OAuth callbacks, public pages, or an external REST shape that cannot be represented cleanly as an action. Do not create `/api/*` routes that mostly wrap or re-export actions.
136
-
137
- For a read-only status endpoint, keep it as an action:
138
-
139
- ```ts filename="actions/get-status.ts"
140
- import { defineAction } from "@agent-native/core/action";
141
- import { z } from "zod";
142
-
143
- export default defineAction({
144
- description: "Return API service status",
145
- schema: z.object({
146
- service: z.string().optional(),
147
- }),
148
- http: { method: "GET" },
149
- run: async ({ service }) => ({
150
- ok: true,
151
- service: service ?? "api",
152
- checkedAt: new Date().toISOString(),
153
- }),
154
- });
155
- ```
156
-
157
- Call it directly over HTTP:
158
-
159
- ```http
160
- GET /_agent-native/actions/get-status?service=billing
161
- ```
162
-
163
- For `POST` and other write actions, send a JSON body to the same action path.
164
-
165
- ## One-Shot Text Completion {#complete-text}
166
-
167
- Most AI work should go through the agent chat so users can see, steer, and audit
168
- what happened. For narrow server-side transforms that intentionally do not need
169
- tools, chat history, or run state, use `completeText()` as an explicit escape
170
- hatch.
171
-
172
- ```ts filename="actions/classify-message.ts"
173
- import { defineAction } from "@agent-native/core/action";
174
- import { completeText } from "@agent-native/core/server";
175
- import { z } from "zod";
176
-
177
- export default defineAction({
178
- description: "Classify a short message",
179
- schema: z.object({ body: z.string() }),
180
- run: async ({ body }) => {
181
- const result = await completeText({
182
- systemPrompt:
183
- "Return exactly one label: urgent, follow-up, waiting, or archive.",
184
- input: body,
185
- maxOutputTokens: 16,
186
- temperature: 0,
187
- });
188
-
189
- return { label: result.text.trim() };
190
- },
191
- });
192
- ```
193
-
194
- `completeText()` runs through the same configured engine layer as the agent
195
- chat, including Builder, Anthropic, AI SDK providers, user/app model defaults,
196
- request-scoped secrets, and engine-normalized errors. It is server-only; do not
197
- call model providers from client code. If the operation is user-facing, wrap it
198
- in an action so the UI and agent share the same capability.
199
-
200
- ## Request Context And Access {#request-context}
201
-
202
- Actions mounted by the framework automatically run with request context. Custom routes do not. If a custom route reads or writes ownable resources, load the session and wrap the work:
203
-
204
- <AnnotatedCode
205
- id="doc-block-z4ll"
206
- title="Scoping a custom route to the request user"
207
- filename="server/routes/api/projects.get.ts"
208
- language="ts"
209
- code={
210
- 'import { defineEventHandler, createError } from "h3";\nimport { getSession, runWithRequestContext } from "@agent-native/core/server";\nimport { getDb } from "../../db/index.js";\nimport { accessFilter } from "@agent-native/core/sharing";\nimport * as schema from "../../db/schema";\n\nexport default defineEventHandler(async (event) => {\n const session = await getSession(event);\n if (!session?.email) {\n throw createError({ statusCode: 401, statusMessage: "Unauthorized" });\n }\n\n return runWithRequestContext(\n { userEmail: session.email, orgId: session.orgId },\n async () => {\n const db = getDb();\n return db\n .select()\n .from(schema.projects)\n .where(accessFilter(schema.projects, schema.projectShares));\n },\n );\n});'
211
- }
212
- annotations={[
213
- {
214
- lines: "7-10",
215
- label: "Custom routes have no auto-context",
216
- note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.",
217
- },
218
- {
219
- lines: "12-13",
220
- label: "Establish request context",
221
- note: "`runWithRequestContext` makes the user/org available to scoping helpers for the duration of the work.",
222
- },
223
- {
224
- lines: "18-19",
225
- label: "Scope ownable reads",
226
- note: "`accessFilter` constrains the query to rows the caller may see. Never run an unscoped `db.select().from(ownableTable)` here.",
227
- },
228
- ]}
229
- />
230
-
231
- `getDb` is created per app via `createGetDb(schema)` in `server/db/index.ts`, so custom routes import it from the template (`../../db/index.js`), not from `@agent-native/core/db`; see [Database — Where the DB Client Lives](/docs/database#db-client). Do not run unscoped `db.select().from(ownableTable)` in custom routes.
232
-
233
- ## Server Plugins {#server-plugins}
234
-
235
- Plugins live in `server/plugins/` and run at startup. Use them for migrations, provider setup, recurring jobs, integration adapters, and framework plugin configuration.
236
-
237
- ```ts filename="server/plugins/db.ts"
238
- import { runMigrations } from "@agent-native/core/db";
239
-
240
- export default runMigrations(
241
- [
242
- {
243
- version: 1,
244
- sql: `CREATE TABLE IF NOT EXISTS projects (
245
- id TEXT PRIMARY KEY,
246
- title TEXT NOT NULL,
247
- owner_email TEXT NOT NULL,
248
- org_id TEXT,
249
- created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
250
- )`,
251
- },
252
- ],
253
- { table: "my_app_migrations" },
254
- );
255
- ```
256
-
257
- Migrations must be additive. Never put destructive SQL in startup plugins.
258
-
259
- ## Framework-Mounted Routes {#framework-routes}
260
-
261
- The framework mounts its own routes under `/_agent-native/`. Treat that namespace as reserved. This table is representative, not exhaustive — it lists the routes most templates touch directly.
262
-
263
- | Route prefix | Purpose |
264
- | -------------------------------- | ------------------------------------------------------------------------------- |
265
- | `/_agent-native/actions/:name` | Action HTTP endpoints |
266
- | `/_agent-native/agent-chat` | Agent chat loop |
267
- | `/_agent-native/poll` | SQL-backed UI sync |
268
- | `/_agent-native/runs` | Progress primitive — the `RunsTray` polls this; see [Progress](/docs/progress) |
269
- | `/_agent-native/resources/*` | Workspace resources |
270
- | `/_agent-native/extensions/*` | Runtime extensions and extension proxy (legacy alias: `/_agent-native/tools/*`) |
271
- | `/_agent-native/integrations/*` | Messaging/webhook integrations |
272
- | `/_agent-native/a2a` | Agent-to-agent JSON-RPC |
273
- | `/mcp` | MCP endpoint |
274
- | `/_agent-native/onboarding/*` | Setup checklist |
275
- | `/_agent-native/observability/*` | Traces, feedback, evals, experiments |
276
- | `/_agent-native/file-upload` | File upload provider endpoint |
277
-
278
- Custom app routes should use `/api/*`, public app paths, or provider-specific callback paths that do not collide with `/_agent-native/`.
279
-
280
- ## SQL-Backed Sync {#sync}
281
-
282
- Agent-native does not rely on filesystem watchers or sticky in-memory state. When actions or framework helpers mutate data, the database sync version increments. The client `useDbSync()` hook polls `/_agent-native/poll` and invalidates React Query caches.
283
-
284
- 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.
285
-
286
- <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."}>
287
-
288
- ```html
289
- <div class="diagram-sync">
290
- <div class="diagram-box" data-rough>
291
- Action / helper<br /><small class="diagram-muted">mutates data</small>
292
- </div>
293
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
294
- <div class="diagram-panel" data-rough>
295
- <strong>SQL database</strong
296
- ><small class="diagram-muted">sync version increments</small>
297
- </div>
298
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&larr;</div>
299
- <div class="diagram-col">
300
- <div class="diagram-node">
301
- useDbSync()<br /><small class="diagram-muted"
302
- >polls /_agent-native/poll</small
303
- >
304
- </div>
305
- <div class="diagram-pill ok">invalidate caches &rarr; UI refreshes</div>
306
- </div>
307
- </div>
308
- ```
309
-
310
- ```css
311
- .diagram-sync {
312
- display: flex;
313
- align-items: center;
314
- gap: 14px;
315
- flex-wrap: wrap;
316
- }
317
- .diagram-sync .diagram-col {
318
- display: flex;
319
- flex-direction: column;
320
- gap: 8px;
321
- align-items: flex-start;
322
- }
323
- .diagram-sync .diagram-arrow {
324
- font-size: 22px;
325
- line-height: 1;
326
- }
327
- ```
328
-
329
- </Diagram>
330
-
331
- <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={[
332
- {
333
- "status": "200",
334
- "description": "Current sync versions keyed by source."
335
- }
336
- ]}>
337
-
338
- `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.
339
-
340
- </Endpoint>
341
-
342
- ## Webhooks {#webhooks}
343
-
344
- Inbound webhooks should verify, persist, and return quickly. Long-running agent work should use the integration queue pattern:
345
-
346
- 1. Verify the platform signature or challenge.
347
- 2. Insert durable work into SQL.
348
- 3. Self-fire a signed processor route.
349
- 4. Return 200 immediately.
350
- 5. Let the fresh processor execution run the agent loop and post the result.
351
-
352
- <Diagram id="doc-block-1v3q81q" title="Integration queue pattern" summary={"The webhook handler returns in milliseconds; a separate signed execution runs the slow agent work."}>
353
-
354
- ```html
355
- <div class="diagram-webhook">
356
- <div class="diagram-box" data-rough>
357
- Inbound webhook<br /><small class="diagram-muted"
358
- >Slack · Stripe · email</small
359
- >
360
- </div>
361
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
362
- <div class="diagram-panel" data-rough>
363
- <strong>Handler</strong>
364
- <div class="diagram-step">
365
- <span class="diagram-pill">1</span
366
- ><small class="diagram-muted">verify signature</small>
367
- </div>
368
- <div class="diagram-step">
369
- <span class="diagram-pill">2</span
370
- ><small class="diagram-muted">insert work into SQL</small>
371
- </div>
372
- <div class="diagram-step">
373
- <span class="diagram-pill">3</span
374
- ><small class="diagram-muted">self-fire processor</small>
375
- </div>
376
- <div class="diagram-step">
377
- <span class="diagram-pill ok">4</span
378
- ><small class="diagram-muted">return 200 now</small>
379
- </div>
380
- </div>
381
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
382
- <div class="diagram-box" data-rough>
383
- Signed processor<br /><small class="diagram-muted"
384
- >runs agent loop, posts result</small
385
- >
386
- </div>
387
- </div>
388
- ```
389
-
390
- ```css
391
- .diagram-webhook {
392
- display: flex;
393
- align-items: center;
394
- gap: 14px;
395
- flex-wrap: wrap;
396
- }
397
- .diagram-webhook .diagram-panel {
398
- display: flex;
399
- flex-direction: column;
400
- gap: 6px;
401
- padding: 14px 16px;
402
- }
403
- .diagram-webhook .diagram-step {
404
- display: flex;
405
- align-items: center;
406
- gap: 8px;
407
- }
408
- .diagram-webhook .diagram-arrow {
409
- font-size: 22px;
410
- line-height: 1;
411
- }
412
- ```
413
-
414
- </Diagram>
415
-
416
- <Callout tone="warning">
417
- Do not rely on unawaited promises after returning a response — serverless
418
- hosts freeze the execution. See [Messaging](/docs/messaging) for the canonical
419
- integration queue.
420
- </Callout>
421
-
422
- ## Advanced: Escape Hatches {#advanced-escape-hatches}
423
-
424
- Most templates never need these. Nitro file routes and the framework's agent
425
- chat plugin already wire up the app server and the production agent handler.
426
- Reach for them only when building a custom server integration outside the
427
- standard template plugin stack.
428
-
429
- ### Programmatic H3 servers {#create-server}
430
-
431
- For custom packages or tests that need an H3 app directly, `createServer()`
432
- returns a preconfigured app and router:
433
-
434
- ```ts
435
- import { createServer } from "@agent-native/core/server";
436
- import { defineEventHandler } from "h3";
437
-
438
- const { app, router } = createServer();
439
-
440
- router.get(
441
- "/api/health",
442
- defineEventHandler(() => ({ ok: true })),
443
- );
444
- ```
445
-
446
- ### Production agent handler {#agent-handler}
447
-
448
- The framework's agent chat plugin already mounts the production agent handler
449
- for templates. Only call `createProductionAgentHandler()` directly when building
450
- a custom server integration outside the standard template plugin stack —
451
- otherwise customize the agent through `AGENTS.md`, skills, actions, and the
452
- agent chat plugin.
453
-
454
- ```ts
455
- import { createProductionAgentHandler } from "@agent-native/core/server";
456
-
457
- const handler = createProductionAgentHandler({
458
- scripts,
459
- systemPrompt: "You are the app agent...",
460
- });
461
- ```
462
-
463
- ## What's next
464
-
465
- - [**Actions**](/docs/actions) — the default operation surface; reach for a custom route only when it doesn't fit
466
- - [**Database**](/docs/database) — the `getDb()`/`schema` pattern custom routes and actions both import
467
- - [**Security**](/docs/security#access-guards) — `accessFilter`/`assertAccess` and the full data-scoping model
468
- - [**Messaging**](/docs/messaging) — the canonical inbound-webhook integration queue pattern
469
- - [**MCP Protocol**](/docs/mcp-protocol) — the `/mcp` endpoint this server mounts automatically