@agent-native/core 0.141.4 → 0.141.6
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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +13 -6
- package/corpus/templates/analytics/actions/get-first-party-analytics-health.ts +15 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
- package/corpus/templates/analytics/app/i18n-data.ts +328 -0
- package/corpus/templates/analytics/app/pages/DataSources.tsx +66 -30
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-now-repair-automatically-after-.md +6 -0
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +342 -0
- package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +8 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +11 -29
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +55 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +117 -31
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +15 -9
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +5 -16
- package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +61 -0
- package/corpus/templates/analytics/server/plugins/db.ts +58 -187
- package/corpus/templates/analytics/server/routes/api/analytics-rollups/backfill.post.ts +53 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-overlays-now-use-clean-surfaces-without-clipped-shadow.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -2
- package/corpus/templates/clips/desktop/src/styles.css +3 -54
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -3
- package/corpus/templates/design/actions/get-design-system.ts +3 -1
- package/corpus/templates/design/changelog/2026-08-05-missing-design-systems-now-return-a-clear-not-found-response.md +6 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +5 -5
- package/corpus/templates/slides/changelog/2026-08-05-new-presentation-prompts-now-clearly-describe-presentation-g.md +6 -0
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +10 -3
- package/dist/client/error-format.d.ts +1 -0
- package/dist/client/error-format.js +4 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/docs/AGENTS.md +1 -1
- package/docs/content/actions.mdx +1 -1
- package/docs/content/agent-surfaces.mdx +1 -1
- package/docs/content/agents.mdx +9 -0
- package/docs/content/database.mdx +2 -2
- package/docs/content/drop-in-agent.mdx +1 -1
- package/docs/content/notifications.mdx +1 -1
- package/docs/content/routing.mdx +1 -1
- package/docs/content/server-database.mdx +437 -0
- package/docs/content/server-middleware.mdx +151 -0
- package/docs/content/server-overview.mdx +234 -0
- package/docs/content/server-plugins.mdx +147 -0
- package/docs/content/server-routes.mdx +194 -0
- package/package.json +1 -1
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +0 -6
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +0 -6
- package/docs/content/server.mdx +0 -469
|
@@ -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">→</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() · Drizzle ORM</small>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</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">→</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">←</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 → 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
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Middleware"
|
|
3
|
+
description: "Nitro middleware in server/middleware/ — what it is, how to write your own, and how the built-in auth guard works."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Middleware
|
|
7
|
+
|
|
8
|
+
Middleware is code that runs on every incoming request before it reaches a route handler. It sits between callers and routes, making it the right place for cross-cutting concerns: authentication, logging, CORS headers, rate limiting, or anything that needs to apply regardless of which route handles the request.
|
|
9
|
+
|
|
10
|
+
<Diagram id="doc-block-server-middleware-1" title="Where middleware runs" summary="Middleware intercepts every request before it reaches a route. Allowed requests continue to the route handler; blocked requests get a redirect or error response.">
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<div class="diagram-mw">
|
|
14
|
+
<div class="diagram-col">
|
|
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">→</div>
|
|
24
|
+
<div class="diagram-panel" data-rough>
|
|
25
|
+
<strong>Middleware</strong>
|
|
26
|
+
<small class="diagram-muted">server/middleware/</small>
|
|
27
|
+
<small class="diagram-muted">runs on every request</small>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="mw-outcomes">
|
|
30
|
+
<div class="mw-outcome">
|
|
31
|
+
<span class="mw-label diagram-muted">allowed</span>
|
|
32
|
+
<span class="mw-arrow diagram-muted" aria-hidden="true">→</span>
|
|
33
|
+
<div class="diagram-box" data-rough>Routes & handlers</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="mw-outcome">
|
|
36
|
+
<span class="mw-label diagram-muted">blocked</span>
|
|
37
|
+
<span class="mw-arrow diagram-muted" aria-hidden="true">→</span>
|
|
38
|
+
<div class="diagram-box" data-rough>Redirect / error</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```css
|
|
45
|
+
.diagram-mw {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: 14px;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
}
|
|
51
|
+
.diagram-mw .diagram-col {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 10px;
|
|
55
|
+
}
|
|
56
|
+
.diagram-mw .diagram-panel {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 4px;
|
|
60
|
+
padding: 12px 16px;
|
|
61
|
+
}
|
|
62
|
+
.diagram-mw .diagram-arrow {
|
|
63
|
+
font-size: 22px;
|
|
64
|
+
line-height: 1;
|
|
65
|
+
}
|
|
66
|
+
.mw-outcomes {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 10px;
|
|
70
|
+
}
|
|
71
|
+
.mw-outcome {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 8px;
|
|
75
|
+
}
|
|
76
|
+
.mw-label {
|
|
77
|
+
font-size: 11px;
|
|
78
|
+
min-width: 44px;
|
|
79
|
+
text-align: right;
|
|
80
|
+
}
|
|
81
|
+
.mw-arrow {
|
|
82
|
+
font-size: 16px;
|
|
83
|
+
line-height: 1;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
</Diagram>
|
|
88
|
+
|
|
89
|
+
Files in `server/middleware/` are picked up automatically by Nitro — no registration required. Every file in that directory runs for every request, in alphabetical order.
|
|
90
|
+
|
|
91
|
+
## Writing Middleware {#writing-middleware}
|
|
92
|
+
|
|
93
|
+
A middleware file exports a `defineEventHandler`. To let the request continue, return nothing. To block it, throw a `createError` or return a redirect with `sendRedirect`.
|
|
94
|
+
|
|
95
|
+
```ts filename="server/middleware/cors.ts"
|
|
96
|
+
import { defineEventHandler, setResponseHeader } from "h3";
|
|
97
|
+
|
|
98
|
+
export default defineEventHandler((event) => {
|
|
99
|
+
setResponseHeader(event, "Access-Control-Allow-Origin", "*");
|
|
100
|
+
// returning nothing passes the request through to the route handler
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Multiple middleware files run in alphabetical filename order before the matched route handler. Each middleware can inspect or modify the request, set response headers, or short-circuit the response entirely.
|
|
105
|
+
|
|
106
|
+
For more middleware patterns — conditional logic, async operations, response interception — see the [Nitro middleware docs](https://nitro.build/guide/routing#middleware) and the [H3 docs](https://h3.unjs.io).
|
|
107
|
+
|
|
108
|
+
## The Auth Middleware {#auth-middleware}
|
|
109
|
+
|
|
110
|
+
Every agent-native app ships `server/middleware/auth.ts`. It enforces authentication across all routes, including public page routes and custom `/api/*` routes that would otherwise bypass the framework's built-in auth.
|
|
111
|
+
|
|
112
|
+
### The Auth Guard {#auth-guard}
|
|
113
|
+
|
|
114
|
+
```ts filename="server/middleware/auth.ts"
|
|
115
|
+
import { runAuthGuard } from "@agent-native/core/server";
|
|
116
|
+
import { defineEventHandler } from "h3";
|
|
117
|
+
|
|
118
|
+
export default defineEventHandler(async (event) => {
|
|
119
|
+
return runAuthGuard(event);
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`runAuthGuard` checks every request against the auth configuration set up by the [auth plugin](/docs/server-plugins#auth-plugin). If the request is unauthenticated and the path is not in `publicPaths`, it redirects to the sign-in page.
|
|
124
|
+
|
|
125
|
+
### Why Middleware, Not a Plugin or Route Guard {#why-middleware}
|
|
126
|
+
|
|
127
|
+
The framework handler's built-in middleware registry is scoped to `/_agent-native/*` routes. Without a separate middleware file, page routes (`/`, `/settings`) and custom API routes (`/api/*`) bypass authentication entirely.
|
|
128
|
+
|
|
129
|
+
Plugins run at startup, not per-request, so they cannot enforce per-request auth. A route-level guard in each custom route handler is error-prone and easy to forget. Middleware in `server/middleware/` is the one place that runs for every request regardless of path.
|
|
130
|
+
|
|
131
|
+
### Adding Public Paths {#public-paths}
|
|
132
|
+
|
|
133
|
+
To allow unauthenticated access to specific paths, pass them to `createAuthPlugin` in `server/plugins/auth.ts` — not to the middleware directly:
|
|
134
|
+
|
|
135
|
+
```ts filename="server/plugins/auth.ts"
|
|
136
|
+
import { createAuthPlugin } from "@agent-native/core/server";
|
|
137
|
+
|
|
138
|
+
export default createAuthPlugin({
|
|
139
|
+
publicPaths: ["/public", "/api/webhooks/stripe"],
|
|
140
|
+
marketing: { appName: "My App", tagline: "..." },
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`runAuthGuard` reads the configured public paths from the auth plugin at request time.
|
|
145
|
+
|
|
146
|
+
## What's next {#whats-next}
|
|
147
|
+
|
|
148
|
+
- [**Plugins**](/docs/server-plugins): auth plugin configuration that the middleware enforces
|
|
149
|
+
- [**Routes**](/docs/server-routes): custom routes that are protected by this middleware
|
|
150
|
+
- [**Security**](/docs/security): data scoping, access guards, and the full auth model
|
|
151
|
+
- [**Nitro middleware docs**](https://nitro.build/guide/routing#middleware): full middleware API and patterns
|