From a0dc3dcdd2e02b03f77ac3e3ea5a5245976cca9d Mon Sep 17 00:00:00 2001 From: Wes Reid Date: Tue, 4 Aug 2026 13:39:17 -0700 Subject: [PATCH 1/6] Add draft Server section docs to Core Architecture Adds five new draft MDX pages (server-overview, server-database, server-middleware, server-plugins, server-routes) and an Agents placeholder, wired into the nav under a draft Server group. Pages are hidden in production and visible with VITE_SHOW_DRAFTS=true. Includes nav keys in all locale files and an updated i18n doc-coverage baseline to exempt draft pages from the localized-doc requirement. --- packages/core/docs/content/agents.mdx | 9 + .../core/docs/content/server-database.mdx | 313 ++++++++++++++++++ .../core/docs/content/server-middleware.mdx | 51 +++ .../core/docs/content/server-overview.mdx | 235 +++++++++++++ packages/core/docs/content/server-plugins.mdx | 120 +++++++ packages/core/docs/content/server-routes.mdx | 195 +++++++++++ packages/core/docs/content/server.mdx | 2 +- packages/docs/app/components/docsNavItems.ts | 50 +++ packages/docs/app/i18n/ar-SA.ts | 6 + packages/docs/app/i18n/de-DE.ts | 6 + packages/docs/app/i18n/en-US.ts | 6 + packages/docs/app/i18n/es-ES.ts | 6 + packages/docs/app/i18n/fr-FR.ts | 6 + packages/docs/app/i18n/hi-IN.ts | 6 + packages/docs/app/i18n/ja-JP.ts | 6 + packages/docs/app/i18n/ko-KR.ts | 6 + packages/docs/app/i18n/pt-BR.ts | 6 + packages/docs/app/i18n/zh-CN.ts | 6 + packages/docs/app/i18n/zh-TW.ts | 6 + .../i18n-localized-doc-coverage-baseline.txt | 60 ++++ 20 files changed, 1100 insertions(+), 1 deletion(-) create mode 100644 packages/core/docs/content/agents.mdx create mode 100644 packages/core/docs/content/server-database.mdx create mode 100644 packages/core/docs/content/server-middleware.mdx create mode 100644 packages/core/docs/content/server-overview.mdx create mode 100644 packages/core/docs/content/server-plugins.mdx create mode 100644 packages/core/docs/content/server-routes.mdx diff --git a/packages/core/docs/content/agents.mdx b/packages/core/docs/content/agents.mdx new file mode 100644 index 0000000000..3b91d5b64f --- /dev/null +++ b/packages/core/docs/content/agents.mdx @@ -0,0 +1,9 @@ +--- +title: "Agents" +description: "Agent configuration, instructions, and the surfaces agents operate through." +draft: true +--- + +# Agents + +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. diff --git a/packages/core/docs/content/server-database.mdx b/packages/core/docs/content/server-database.mdx new file mode 100644 index 0000000000..fddcf6cc1e --- /dev/null +++ b/packages/core/docs/content/server-database.mdx @@ -0,0 +1,313 @@ +--- +title: "Database" +description: "Connect a portable SQL database to your agent-native app and write provider-agnostic Drizzle code." +draft: true +--- + +# Database + +Agent-native apps use [Drizzle ORM](https://orm.drizzle.team) and support portable SQL backends. For anything beyond local development, connect a persistent SQL database — Postgres, libSQL/Turso, or another Drizzle-compatible backend — by setting `DATABASE_URL`. When that variable is unset, the app falls back to a zero-config local SQLite file so you can start developing immediately. For local development that should behave like Postgres without running a separate database server, opt into PGlite with `DATABASE_URL=pglite:./data/pglite`. + + + +```html +
+
+ @agent-native/core/db/schematable · text · integer · real · now+ Drizzle query DSL +
+ +
+ DATABASE_URL
dialect auto-detected +
+ +
+ Postgres
Neon · Supabase
libSQL / TursoCloudflare D1SQLite file
unset = local dev only
PGlite
local Postgres opt-in
+
+
+``` + +```css +.diagram-db { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} +.diagram-db .center { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 14px 16px; +} +.diagram-db .diagram-arrow { + font-size: 22px; + line-height: 1; +} +.diagram-db .diagram-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} +``` + +
+ +## Local default: SQLite file {#default-sqlite} + +When `DATABASE_URL` is not set, the app creates a SQLite database at `data/app.db`. This is the zero-config default for local development — no setup required. It is meant for development only; for production, set `DATABASE_URL` to a persistent SQL database. + +Do not rely on that local file for deployed apps. Containers, serverless functions, and preview environments may reset their filesystem, which means a local SQLite file can disappear between restarts. Set `DATABASE_URL` to a persistent hosted database before production use. + +## Local Postgres Opt-In: PGlite {#local-pglite} + +Install the optional PGlite package, then set `DATABASE_URL=pglite:./data/pglite` to run the app against [PGlite](https://pglite.dev/), a local WASM Postgres database: + +```bash +pnpm add @electric-sql/pglite@^0.5.3 +``` + +This keeps local development on the Postgres dialect, including Postgres schema helpers and migrations, without requiring Docker or a hosted database. + +PGlite is still local development storage. Treat it like the SQLite fallback for durability and sharing: use it to test Postgres-shaped behavior on your machine, then set `DATABASE_URL` to a persistent hosted database for production, previews, or any shared environment. + +## Connecting a Production Database {#production} + +Set `DATABASE_URL` in your `.env` file or deploy-provider environment to connect a hosted database. Turso is not required; use whichever Drizzle-compatible SQL backend fits your deployment: + +```bash +# Neon Postgres +DATABASE_URL=postgres://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/mydb?sslmode=require + +# Supabase Postgres +DATABASE_URL=postgres://postgres.xxxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres + +# Plain Postgres +DATABASE_URL=postgres://user:pass@localhost:5432/mydb + +# Local PGlite (Postgres dialect, local development only) +DATABASE_URL=pglite:./data/pglite + +# Turso (libSQL) +DATABASE_URL=libsql://my-db-org.turso.io +DATABASE_AUTH_TOKEN=your-token +``` + +The framework auto-detects the dialect from the URL and configures Drizzle accordingly. The built-in adapters cover Postgres URLs, local PGlite URLs, libSQL/Turso URLs, SQLite file URLs, and Cloudflare D1 bindings. Common production choices include Neon, Supabase, Turso/libSQL, plain Postgres, durable SQLite, and Builder.io-managed environments when available. + +## Builder.io Managed Database {#builder-managed} + +_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. + +## Where the DB Client Lives {#db-client} + +Each template creates a lazy, singleton Drizzle client by calling `createGetDb(schema)` from `@agent-native/core/db`. The canonical location is `server/db/index.ts`: + +```ts filename="server/db/index.ts" +import { createGetDb } from "@agent-native/core/db"; +import * as schema from "./schema.js"; + +export const getDb = createGetDb(schema); +``` + +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. + +## Dialect-Agnostic Schema And Queries {#schema} + +App database code should use Drizzle's schema and query DSL so it can run across providers. Never write SQLite-only syntax (`INSERT OR REPLACE`, `AUTOINCREMENT`, `datetime('now')`) or Postgres-only syntax in product code. + +Use the framework's schema helpers from `@agent-native/core/db/schema`: + +```ts +import { table, text, integer, real, now } from "@agent-native/core/db/schema"; + +export const tasks = table("tasks", { + id: text("id").primaryKey(), + title: text("title").notNull(), + priority: integer("priority").notNull().default(0), + weight: real("weight"), + done: integer("done", { mode: "boolean" }).notNull().default(false), + ownerEmail: text("owner_email").notNull(), + createdAt: text("created_at").notNull().default(now()), +}); +``` + +| Helper | Purpose | +| --------- | --------------------------------------------------------------- | +| `table` | Define a table — delegates to `pgTable` or `sqliteTable` | +| `text` | Text column, supports `{ enum: [...] }` | +| `integer` | Integer column, `{ mode: "boolean" }` maps to Postgres boolean | +| `real` | Float column — `real` on SQLite, `double precision` on Postgres | +| `now` | Dialect-agnostic current timestamp for `.default(now())` | + +The `tasks` table above defines the same columns on every backend: + + + +Never import from `drizzle-orm/sqlite-core` or `drizzle-orm/pg-core` directly. Always use `@agent-native/core/db/schema`. + +Tables that store user-facing data must include an `owner_email` column so the framework's SQL-level scoping can filter rows to the authenticated user — see [Security](/docs/security#data-scoping). 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 [Sharing](/docs/sharing#building). + +For reads and writes, use Drizzle's query builder and portable operators from `drizzle-orm`: + +```ts +import { and, desc, eq } from "drizzle-orm"; +import { getDb } from "../server/db/index.js"; +import { tasks } from "../server/db/schema.js"; + +const db = getDb(); + +const openTasks = await db + .select() + .from(tasks) + .where(and(eq(tasks.ownerEmail, userEmail), eq(tasks.done, false))) + .orderBy(desc(tasks.createdAt)); + +await db.update(tasks).set({ done: true }).where(eq(tasks.id, taskId)); +``` + +## Raw SQL Escape Hatches {#raw-sql} + +Raw SQL is not the default app-code API. Use it only for additive migrations, health checks, carefully reviewed advanced queries that Drizzle cannot express, or one-off maintenance. Keep it parameterized and dialect-agnostic. For timestamps in Drizzle schemas, prefer `.default(now())`; for migration SQL, use `runMigrations()` so framework-supported compatibility rewrites and dialect-gated statements stay centralized. + +For cases where you truly need raw SQL outside of Drizzle queries: + +- `getDbExec()` — auto-converts `?` params to `$1` for Postgres +- `isPostgres()` — runtime dialect check +- `intType()` — returns the correct integer type for the current dialect + +## Migrations and Schema Updates {#migrations} + +In hosted environments, multiple deployment previews, branches, and the production server share the same underlying database. Therefore, database schema updates must follow strict constraints to avoid data loss and service disruption. + +### The "Zero Destructive Changes" Rule + +All database schema updates must be **strictly additive**. + +- **Do not drop tables or columns.** +- **Do not rename tables or columns.** Renaming a column or table looks like a drop + create sequence to Drizzle, which will permanently delete your existing production data. +- If a column needs to be renamed or replaced, add the new column alongside the old one, update your application code to read from/write to both, migrate the data, and only retire the old column in a later release once no active deployments are referencing it. + + + **Never run `drizzle-kit push` against a production database.** Template + database schemas only define app-specific domain tables; they do not define + central framework tables (`user`, `session`, `application_state`, etc.). If + you run `drizzle-kit push` against production, Drizzle will detect these + framework tables as "not in schema" and attempt to drop them, causing + immediate system-wide failure and data loss. + + +### Safe Migration Path + +Instead of pushing directly, schema changes should be applied via SQL migrations executed at application startup. Implement additive migrations within a server plugin (e.g., `server/plugins/db.ts`) by invoking the framework's `runMigrations()` helper: + + + +## Environment Variables {#environment-variables} + +| Variable | Purpose | +| --------------------- | ------------------------------------------------------------------------------------------------------- | +| `DATABASE_URL` | Persistent SQL connection string (unset = local SQLite; `pglite:./data/pglite` = local Postgres opt-in) | +| `DATABASE_AUTH_TOKEN` | Auth token for providers that require a separate token, such as Turso/libSQL | + +## What's next + +- [**Security — Data Scoping**](/docs/security#data-scoping) — how `owner_email` and access helpers scope reads and writes +- [**Sharing**](/docs/sharing#building) — `ownableColumns()` and the visibility model for shared resources +- [**Server**](/docs/server#request-context) — how actions and custom routes each import `getDb` +- [**Deployment**](/docs/deployment#persistent-database) — connecting a persistent database per deploy target +- [**Actions**](/docs/actions#access-control) — a complete, paste-ready action that reads and writes through `getDb`/`schema` diff --git a/packages/core/docs/content/server-middleware.mdx b/packages/core/docs/content/server-middleware.mdx new file mode 100644 index 0000000000..a60972d762 --- /dev/null +++ b/packages/core/docs/content/server-middleware.mdx @@ -0,0 +1,51 @@ +--- +title: "Middleware" +description: "Nitro middleware in server/middleware/ — what it is, how the auth guard works, and why it lives there instead of in a plugin or route." +draft: true +--- + +# Middleware + +Nitro middleware runs on every incoming request before it reaches a route handler. Files in `server/middleware/` are picked up automatically — no registration needed. + +Every agent-native app ships one middleware file: `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. + +## The Auth Guard {#auth-guard} + +```ts filename="server/middleware/auth.ts" +import { runAuthGuard } from "@agent-native/core/server"; +import { defineEventHandler } from "h3"; + +export default defineEventHandler(async (event) => { + return runAuthGuard(event); +}); +``` + +`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. + +## Why Middleware, Not a Plugin or Route Guard {#why-middleware} + +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 — only framework routes would be protected. + +Plugins run at startup, not per-request, so they can't 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. + +## Adding Public Paths {#public-paths} + +To allow unauthenticated access to specific paths, pass them to `createAuthPlugin` in `server/plugins/auth.ts` — not to the middleware directly: + +```ts filename="server/plugins/auth.ts" +import { createAuthPlugin } from "@agent-native/core/server"; + +export default createAuthPlugin({ + publicPaths: ["/public", "/api/webhooks/stripe"], + marketing: { appName: "My App", tagline: "..." }, +}); +``` + +`runAuthGuard` reads the configured public paths from the auth plugin at request time. + +## What's next + +- [**Plugins**](/docs/server-plugins) — auth plugin configuration that the middleware enforces +- [**Routes**](/docs/server-routes) — custom routes that are protected by this middleware +- [**Security**](/docs/security) — data scoping, access guards, and the full auth model diff --git a/packages/core/docs/content/server-overview.mdx b/packages/core/docs/content/server-overview.mdx new file mode 100644 index 0000000000..c309985f85 --- /dev/null +++ b/packages/core/docs/content/server-overview.mdx @@ -0,0 +1,235 @@ +--- +title: "Server" +description: "The Nitro server layer in an agent-native app: what it's for, when to reach for custom routes, and how its pieces connect." +draft: true +--- + +# Server + +The `server/` directory is the infrastructure layer of an agent-native app. It handles startup tasks, authentication, and the HTTP surfaces that actions don't cover. Most product behavior belongs in [Actions](/docs/actions). The server is what makes those actions available. + + + +```html +
+
+
Browser / UI
+
Agent loop
+
+ External clients
HTTP · MCP · A2A +
+
+ +
+ Nitro server +
+ Actionsprimary path +
+
or
+
+ MiddlewareRoutes +
+
+ +
+ SQL database
Drizzle +
+
+``` + +```css +.diagram-srv { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} +.diagram-srv .diagram-col { + display: flex; + flex-direction: column; + gap: 10px; +} +.diagram-srv .diagram-panel { + display: flex; + flex-direction: column; + gap: 8px; + padding: 14px 16px; +} +.diagram-srv .diagram-arrow { + font-size: 22px; + line-height: 1; +} +.srv-path { + display: flex; + align-items: center; + gap: 8px; +} +.srv-or { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + padding-left: 2px; +} +.srv-inner-arrow { + font-size: 14px; + line-height: 1; +} +``` + +
+ +## Where the Server Fits {#role} + +In a traditional web app, the server is where most logic lives. Business rules, data access, and presentation all happen inside route handlers. Agent-native apps work differently. Logic lives in actions, which the agent and UI share as equal partners. The server's job is coordination: run database migrations, configure authentication, mount the agent loop, and handle the narrow set of HTTP concerns that actions can't express on their own. + + + +```html +
+
+
Traditional
+
Browser
+ +
+ Server + logic lives here +
+ Route handlers + Business logic + Data access +
+
+ +
Database
+
+
vs
+
+
Agent Native
+
+
Browser / UI
+
Agent
+
+ +
+ Server + coordination only +
+ Actions + Auth + Migrations +
+
+ +
Database
+
+
+``` + +```css +.diagram-cmp { + display: flex; + align-items: flex-start; + gap: 28px; + flex-wrap: wrap; +} +.cmp-col { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1; + min-width: 160px; +} +.cmp-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 600; +} +.cmp-callers { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} +.cmp-vs { + font-size: 13px; + font-weight: 600; + padding-top: 52px; +} +.diagram-cmp .diagram-panel { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 16px; + align-self: stretch; + align-items: flex-start; +} +.cmp-pills { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.cmp-tag { + font-size: 12px; + opacity: 0.7; + margin-right: 10px; +} +.cmp-arrow { + font-size: 18px; + line-height: 1; +} +``` + +
+ +Browser requests, the agent loop, and external API clients all route through the same Nitro server and read from the same SQL database. There is no separate agent backend or UI backend. The server layer is what exposes actions to the outside world. + +## What's in the server/ directory {#server-directory} + +These are the directories and files inside `server/` that every agent-native app ships. Each corresponds to a specific piece of the server layer: what it does, when to reach for it, and how it connects to the rest of the framework. + +| Page | What it covers | +| ----------------------------------------- | ----------------------------------------------------------------------------------- | +| **[Database](/docs/server-database)** | Schema definition with `table()` helpers, `getDb`, and ownable table patterns | +| **[Middleware](/docs/server-middleware)** | Auth guards in `server/middleware/` and why they run there instead of in a plugin | +| **[Plugins](/docs/server-plugins)** | The three startup plugins every app ships: db migrations, auth, and agent-chat | +| **[Routes](/docs/server-routes)** | File-based routes, the SSR catch-all, and scoping custom routes to the request user | + +## Nitro {#nitro} + +[Nitro](https://nitro.build) is the server toolkit that powers the agent-native HTTP layer. Most of what you'll configure in `server/` is Nitro: file-based routes, startup plugins, and middleware. + +- **File-based routing**: the filename maps directly to an HTTP method and URL path, with no registration needed. `server/routes/api/health.get.ts` becomes `GET /api/health` automatically. Visit [Routes](/docs/server-routes) for more details. +- **Plugins**: code that runs once at server startup, before any request is served. Database migrations, auth configuration, and the agent-chat mount all happen in `server/plugins/`. Visit [Plugins](/docs/server-plugins) for more details. +- **Middleware**: code that runs on every incoming request before it reaches a route. The auth guard lives in `server/middleware/`. Visit [Middleware](/docs/server-middleware) for more details. + +Nitro is also deployment-agnostic. The same app deploys to Node.js, Cloudflare Workers, Netlify, Vercel, and other runtimes with no code changes. Just set a preset. See [Deployment](/docs/deployment). + +The underlying request/response API is [H3](https://h3.unjs.io). When you see `defineEventHandler`, `createError`, or `getQuery` in custom routes, that's H3. The [Nitro docs](https://nitro.build/guide) and [H3 docs](https://h3.unjs.io) are the reference for anything the framework doesn't cover directly. + +## Default to Actions first {#actions-first} + +Agent Native architecture is different. Whenever you think of a new feature for your application, consider first whether it can be an action. For example, if the UI and agent both need to do something, define an action instead of a custom API route. Actions automatically become: + +- Agent tools. +- Typed frontend hooks. +- HTTP endpoints under `/_agent-native/actions/:name`. +- MCP and A2A-callable tools. +- CLI commands for development. + +Use custom `/api/*` routes only when you need a route-shaped protocol or binary/streaming behavior. For more details, visit [Actions](/docs/actions). + +## What's next + +- [**Actions**](/docs/actions): the default operation surface. Reach for a custom route only when it doesn't fit. +- [**Database**](/docs/server-database): the `getDb()`/schema pattern custom routes and actions both import +- [**Middleware**](/docs/server-middleware): auth guards and the `server/middleware/` directory +- [**Plugins**](/docs/server-plugins): startup plugins for migrations, auth, and the agent +- [**Routes**](/docs/server-routes): file-based routes, request context, and the SSR catch-all diff --git a/packages/core/docs/content/server-plugins.mdx b/packages/core/docs/content/server-plugins.mdx new file mode 100644 index 0000000000..33b8a5254c --- /dev/null +++ b/packages/core/docs/content/server-plugins.mdx @@ -0,0 +1,120 @@ +--- +title: "Plugins" +description: "The three startup plugins every agent-native app ships: database migrations, auth configuration, and the agent-chat plugin." +draft: true +--- + +# Plugins + +Plugins live in `server/plugins/` and run at server startup, before any routes are served. Every app ships three: one that runs database migrations, one that configures authentication, and one that mounts the agent chat loop with your app's actions and system prompt. + +```text +server/plugins/ + db.ts -> run migrations, create tables + auth.ts -> configure auth provider and public paths + agent-chat.ts -> mount the agent with your actions and system prompt +``` + +## Database Plugin {#database-plugin} + +`server/plugins/db.ts` runs schema migrations on startup using `runMigrations` from `@agent-native/core/db`. Migrations must be additive — never drop, rename, or destructively alter tables here. + +```ts filename="server/plugins/db.ts" +import { runMigrations } from "@agent-native/core/db"; + +export default runMigrations( + [ + { + version: 1, + sql: `CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + owner_email TEXT NOT NULL, + org_id TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + )`, + }, + ], + { table: "my_app_migrations" }, +); +``` + +Each entry needs a `version` number and a `sql` string. The `table` option names the migration tracking table — use a name unique to your app so it doesn't collide with framework migrations. See [Database](/docs/database) for the full schema authoring guide. + + + Migrations must be additive. Never put destructive SQL (`DROP TABLE`, + `ALTER TABLE ... DROP COLUMN`, `DELETE FROM`) in startup plugins. + + +## Auth Plugin {#auth-plugin} + +`server/plugins/auth.ts` configures how users sign in. `createAuthPlugin` from `@agent-native/core/server` wires up the auth provider and sets the sign-in page branding: + +```ts filename="server/plugins/auth.ts" +import { createAuthPlugin } from "@agent-native/core/server"; + +export default createAuthPlugin({ + marketing: { + appName: "My App", + tagline: "A short description shown on the sign-in page.", + features: [ + "Feature one", + "Feature two", + "Feature three", + ], + }, +}); +``` + +To open specific routes to unauthenticated users, pass `publicPaths`: + +```ts +export default createAuthPlugin({ + publicPaths: ["/public", "/api/webhook"], + marketing: { appName: "My App", tagline: "..." }, +}); +``` + +The auth plugin configures the guard; the auth middleware in `server/middleware/auth.ts` enforces it on every request. See [Middleware](/docs/server-middleware). + +## Agent-Chat Plugin {#agent-chat-plugin} + +`server/plugins/agent-chat.ts` mounts the agent loop with your app's actions, system prompt, and initial tool set. This is the plugin that makes your app an agent-native app. + +```ts filename="server/plugins/agent-chat.ts" +import { getOrgContext } from "@agent-native/core/org"; +import { + createAgentChatPlugin, + loadActionsFromStaticRegistry, +} from "@agent-native/core/server"; + +import actionsRegistry from "../../.generated/actions-registry.js"; + +export default createAgentChatPlugin({ + appId: "my-app", + actions: loadActionsFromStaticRegistry(actionsRegistry), + initialToolNames: ["view-screen", "navigate"], + resolveOrgId: async (event) => (await getOrgContext(event)).orgId, + systemPrompt: `You are the My App agent. + +Use actions as the source of truth. Start by inspecting the current +screen when context matters.`, +}); +``` + +`loadActionsFromStaticRegistry` picks up every action exported from `actions/` through the generated registry. `initialToolNames` controls which actions the agent has access to at the start of a conversation — it can still call others as needed. + +## Startup Order {#startup-order} + +Nitro runs plugins in filesystem order. Within a single plugin file the default export is awaited before the next plugin starts. The typical order is: + +1. `db.ts` — migrations complete before any route or plugin reads the database +2. `auth.ts` — auth configuration is in place before requests arrive +3. `agent-chat.ts` — agent routes mount after auth is configured + +## What's next + +- [**Database**](/docs/database) — schema helpers and the `getDb` pattern used inside plugins +- [**Middleware**](/docs/server-middleware) — the auth guard that enforces what the auth plugin configures +- [**Routes**](/docs/server-routes) — custom file routes served after plugins complete +- [**Writing Agent Instructions**](/docs/writing-agent-instructions) — how to craft the `systemPrompt` in the agent-chat plugin diff --git a/packages/core/docs/content/server-routes.mdx b/packages/core/docs/content/server-routes.mdx new file mode 100644 index 0000000000..fe05dea1ae --- /dev/null +++ b/packages/core/docs/content/server-routes.mdx @@ -0,0 +1,195 @@ +--- +title: "Routes" +description: "File-based Nitro server routes, the SSR catch-all, naming conventions, and scoping custom routes to the authenticated user." +draft: true +--- + +# Routes + +Server routing in an agent-native app is split across two surfaces. + +**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. + +**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. + +## Framework-Mounted Routes {#framework-routes} + +The framework reserves the `/_agent-native/` namespace for its own endpoints. Never put custom routes here. This table is representative, not exhaustive. + +| Route prefix | Purpose | +| -------------------------------- | ------------------------------------------------------------------------------- | +| `/_agent-native/actions/:name` | Action HTTP endpoints | +| `/_agent-native/agent-chat` | Agent chat loop | +| `/_agent-native/poll` | SQL-backed UI sync | +| `/_agent-native/runs` | Progress primitive. The `RunsTray` polls this; see [Progress](/docs/progress) | +| `/_agent-native/resources/*` | Workspace resources | +| `/_agent-native/extensions/*` | Runtime extensions and extension proxy (legacy alias: `/_agent-native/tools/*`) | +| `/_agent-native/integrations/*` | Messaging/webhook integrations | +| `/_agent-native/a2a` | Agent-to-agent JSON-RPC | +| `/mcp` | MCP endpoint | +| `/_agent-native/onboarding/*` | Setup checklist | +| `/_agent-native/observability/*` | Traces, feedback, evals, experiments | +| `/_agent-native/file-upload` | File upload provider endpoint | + +Custom app routes go in `/api/*` or another path that doesn't collide with `/_agent-native/`. + +## When to Use a Custom Route {#when-custom-routes} + +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. + + + +### Use a custom route + +- File uploads or multipart form data +- Streaming responses or server-sent events (SSE) +- Inbound webhooks that need raw body verification +- OAuth callbacks +- Public unauthenticated pages +- External REST contracts that require a specific URL path or response shape + +### Use an action instead + +- Operations the UI and agent both need +- Reading or writing app data +- HTTP-shaped endpoints (use the `http` option in [Actions](/docs/actions)) +- Anything that should be an agent tool, MCP tool, or CLI command +- Standard JSON request/response APIs + + + +### Example: Inbound webhook {#example-webhook} + +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. + +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. + +```ts filename="server/routes/api/webhooks/events.post.ts" +import { createHmac, timingSafeEqual } from "crypto"; +import { createError, defineEventHandler, getHeader, readRawBody } from "h3"; + +export default defineEventHandler(async (event) => { + const secret = process.env.WEBHOOK_SECRET; + const signature = getHeader(event, "x-webhook-signature"); + const rawBody = await readRawBody(event); + + if (!secret || !signature || !rawBody) { + throw createError({ statusCode: 400, statusMessage: "Bad Request" }); + } + + const expected = createHmac("sha256", secret).update(rawBody).digest("hex"); + if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { + throw createError({ statusCode: 401, statusMessage: "Invalid signature" }); + } + + const payload = JSON.parse(rawBody) as { type: string; data: unknown }; + // handle payload.type ... + + return { received: true }; +}); +``` + +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. + +## File-Based Routes {#file-based-routes} + +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. + +```text +server/routes/ + api/ + items/ + index.get.ts -> GET /api/items + index.post.ts -> POST /api/items + [id].get.ts -> GET /api/items/:id + [id].patch.ts -> PATCH /api/items/:id + [id].delete.ts -> DELETE /api/items/:id + webhooks/ + stripe.post.ts -> POST /api/webhooks/stripe + [...page].get.ts -> SSR catch-all for public pages +``` + +`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. + +Router params like `[id]` are read from `event` with `getRouterParam`: + +```ts filename="server/routes/api/items/[id].get.ts" +import { defineEventHandler, getRouterParam, createError } from "h3"; + +export default defineEventHandler((event) => { + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, statusMessage: "Missing id" }); + // fetch and return the item by id +}); +``` + +### Naming conventions {#naming-conventions} + +| File path (relative to `server/routes/`) | HTTP method | URL path | +| ---------------------------------------- | ----------- | ---------------- | +| `api/items/index.get.ts` | GET | `/api/items` | +| `api/items/index.post.ts` | POST | `/api/items` | +| `api/items/[id].get.ts` | GET | `/api/items/:id` | +| `api/items/[id].patch.ts` | PATCH | `/api/items/:id` | +| `api/items/[id].delete.ts` | DELETE | `/api/items/:id` | +| `api/items/[...slug].get.ts` | GET | `/api/items/*` | + +## Request Context and Access {#request-context} + +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: + + {\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});' + } + annotations={[ + { + lines: "8-11", + label: "Custom routes have no auto-context", + note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.", + }, + { + lines: "13", + label: "Establish request context", + note: "`runWithRequestContext` makes the user/org available to scoping helpers for the duration of the work.", + }, + { + lines: "17-20", + label: "Scope ownable reads", + note: "`accessFilter` constrains the query to rows the caller may see. Never run an unscoped `db.select().from(ownableTable)` here.", + }, + ]} +/> + +`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). + + + Do not run unscoped `db.select().from(ownableTable)` in custom routes. Always + establish request context with `runWithRequestContext` before querying ownable + data. + + +## SSR Catch-All {#ssr-catch-all} + +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: + +```ts filename="server/routes/[...page].get.ts" +import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler"; + +export default createH3SSRHandler( + () => import("virtual:react-router/server-build"), +); +``` + +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. + +## What's next + +- [**Actions**](/docs/actions): the preferred surface for app operations; reach for a route only when it doesn't fit +- [**Plugins**](/docs/server-plugins): startup plugins that run before routes are served +- [**Middleware**](/docs/server-middleware): auth guards that protect all routes including `/api/*` +- [**Security**](/docs/security#access-guards): `accessFilter`/`assertAccess` and the full data-scoping model diff --git a/packages/core/docs/content/server.mdx b/packages/core/docs/content/server.mdx index 0e978affa1..17a1b47855 100644 --- a/packages/core/docs/content/server.mdx +++ b/packages/core/docs/content/server.mdx @@ -211,7 +211,7 @@ Actions mounted by the framework automatically run with request context. Custom } annotations={[ { - lines: "7-10", + lines: "7-11", label: "Custom routes have no auto-context", note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.", }, diff --git a/packages/docs/app/components/docsNavItems.ts b/packages/docs/app/components/docsNavItems.ts index f4559bb4ec..19c3005f54 100644 --- a/packages/docs/app/components/docsNavItems.ts +++ b/packages/docs/app/components/docsNavItems.ts @@ -86,6 +86,43 @@ const NAV_SECTION_CONFIG: NavSectionConfig[] = [ titleKey: "coreArchitecture", items: [ { id: "server", labelKey: "server", slug: "server" }, + { + id: "server-section", + labelKey: "server", + draft: true, + children: [ + { + id: "server-overview", + labelKey: "serverOverview", + slug: "server-overview", + draft: true, + }, + { + id: "server-database", + labelKey: "database", + slug: "server-database", + draft: true, + }, + { + id: "server-middleware", + labelKey: "serverMiddleware", + slug: "server-middleware", + draft: true, + }, + { + id: "server-plugins", + labelKey: "serverPlugins", + slug: "server-plugins", + draft: true, + }, + { + id: "server-routes", + labelKey: "serverRoutes", + slug: "server-routes", + draft: true, + }, + ], + }, { id: "client", labelKey: "client", slug: "client" }, { id: "routing", labelKey: "routing", slug: "routing" }, { id: "actions", labelKey: "actions", slug: "actions" }, @@ -113,6 +150,19 @@ const NAV_SECTION_CONFIG: NavSectionConfig[] = [ { id: "file-uploads", labelKey: "fileUploads", slug: "file-uploads" }, { id: "deployment", labelKey: "deployment", slug: "deployment" }, { id: "progress", labelKey: "progress", slug: "progress" }, + { + id: "agents-group", + labelKey: "agents", + draft: true, + children: [ + { + id: "agents-overview", + labelKey: "agentsOverview", + slug: "agents", + draft: true, + }, + ], + }, ], }, { diff --git a/packages/docs/app/i18n/ar-SA.ts b/packages/docs/app/i18n/ar-SA.ts index 38fa519a40..8bbb3a9c3f 100644 --- a/packages/docs/app/i18n/ar-SA.ts +++ b/packages/docs/app/i18n/ar-SA.ts @@ -1507,6 +1507,10 @@ const arSA = { pureAgentApps: "تطبيقات الأتمتة أولاً", faq: "FAQ", server: "Server", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Client", routing: "Routing", actions: "الإجراءات", @@ -1540,6 +1544,8 @@ const arSA = { realTimeCollaboration: "تعاون فوري", agentResourcesOverview: "نظرة عامة على موارد الوكيل", skills: "المهارات", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Agents و Teams مخصصة", workspaceGovernance: "حوكمة مساحة العمل", recurringJobs: "وظائف متكررة", diff --git a/packages/docs/app/i18n/de-DE.ts b/packages/docs/app/i18n/de-DE.ts index 1b87c971db..43589b727c 100644 --- a/packages/docs/app/i18n/de-DE.ts +++ b/packages/docs/app/i18n/de-DE.ts @@ -1519,6 +1519,10 @@ const deDE = { pureAgentApps: "Automatisierungsorientierte Apps", faq: "FAQ", server: "Server", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Client", routing: "Routing", actions: "Aktionen", @@ -1552,6 +1556,8 @@ const deDE = { realTimeCollaboration: "Echtzeit-Zusammenarbeit", agentResourcesOverview: "Übersicht über Agent-Ressourcen", skills: "Fähigkeiten", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Eigene Agents und Teams", workspaceGovernance: "Workspace-Governance", recurringJobs: "Wiederkehrende Jobs", diff --git a/packages/docs/app/i18n/en-US.ts b/packages/docs/app/i18n/en-US.ts index f86dd2ad5e..0b00b111ff 100644 --- a/packages/docs/app/i18n/en-US.ts +++ b/packages/docs/app/i18n/en-US.ts @@ -1509,6 +1509,10 @@ const enUS = { pureAgentApps: "Automation-First Apps", faq: "FAQ", server: "Server", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Client", routing: "Routing", actions: "Actions", @@ -1542,6 +1546,8 @@ const enUS = { realTimeCollaboration: "Real-Time Collaboration", agentResourcesOverview: "Agent Resources Overview", skills: "Skills", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Custom Agents & Teams", workspaceGovernance: "Workspace Governance", recurringJobs: "Recurring Jobs", diff --git a/packages/docs/app/i18n/es-ES.ts b/packages/docs/app/i18n/es-ES.ts index 717b1d7210..7442fbfa21 100644 --- a/packages/docs/app/i18n/es-ES.ts +++ b/packages/docs/app/i18n/es-ES.ts @@ -1519,6 +1519,10 @@ const esES = { pureAgentApps: "Apps orientadas a la automatización", faq: "FAQ", server: "Servidor", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Cliente", routing: "Rutas", actions: "Acciones", @@ -1552,6 +1556,8 @@ const esES = { realTimeCollaboration: "Colaboración en tiempo real", agentResourcesOverview: "Descripción general de los recursos del agente", skills: "Habilidades", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Agents y equipos personalizados", workspaceGovernance: "Gobernanza del workspace", recurringJobs: "Tareas recurrentes", diff --git a/packages/docs/app/i18n/fr-FR.ts b/packages/docs/app/i18n/fr-FR.ts index ffc188f270..2124017d4c 100644 --- a/packages/docs/app/i18n/fr-FR.ts +++ b/packages/docs/app/i18n/fr-FR.ts @@ -1520,6 +1520,10 @@ const frFR = { pureAgentApps: "Apps orientées automatisation", faq: "FAQ", server: "Serveur", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Client", routing: "Routage", actions: "Opérations", @@ -1553,6 +1557,8 @@ const frFR = { realTimeCollaboration: "Collaboration temps réel", agentResourcesOverview: "Vue d'ensemble des ressources de l'agent", skills: "Compétences", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Agents et équipes personnalisés", workspaceGovernance: "Gouvernance du workspace", recurringJobs: "Tâches récurrentes", diff --git a/packages/docs/app/i18n/hi-IN.ts b/packages/docs/app/i18n/hi-IN.ts index 293be8ff1e..eea7601ac2 100644 --- a/packages/docs/app/i18n/hi-IN.ts +++ b/packages/docs/app/i18n/hi-IN.ts @@ -1509,6 +1509,10 @@ const hiIN = { pureAgentApps: "ऑटोमेशन-फर्स्ट Apps", faq: "FAQ", server: "Server", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Client", routing: "Routing", actions: "क्रियाएँ", @@ -1542,6 +1546,8 @@ const hiIN = { realTimeCollaboration: "Real-time collaboration", agentResourcesOverview: "एजेंट संसाधन अवलोकन", skills: "स्किल्स", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Custom Agents और Teams", workspaceGovernance: "Workspace governance", recurringJobs: "आवर्ती नौकरियाँ", diff --git a/packages/docs/app/i18n/ja-JP.ts b/packages/docs/app/i18n/ja-JP.ts index 021b431a08..722bac3e42 100644 --- a/packages/docs/app/i18n/ja-JP.ts +++ b/packages/docs/app/i18n/ja-JP.ts @@ -1516,6 +1516,10 @@ const jaJP = { pureAgentApps: "自動化ファーストアプリ", faq: "FAQ", server: "サーバー", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "クライアント", routing: "ルーティング", actions: "アクション", @@ -1549,6 +1553,8 @@ const jaJP = { realTimeCollaboration: "リアルタイム共同編集", agentResourcesOverview: "エージェント リソースの概要", skills: "スキル", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "カスタム Agents とチーム", workspaceGovernance: "Workspace ガバナンス", recurringJobs: "定期ジョブ", diff --git a/packages/docs/app/i18n/ko-KR.ts b/packages/docs/app/i18n/ko-KR.ts index 779cb1c213..df85f50ab4 100644 --- a/packages/docs/app/i18n/ko-KR.ts +++ b/packages/docs/app/i18n/ko-KR.ts @@ -1512,6 +1512,10 @@ const koKR = { pureAgentApps: "자동화 우선 앱", faq: "FAQ", server: "서버", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "클라이언트", routing: "라우팅", actions: "작업", @@ -1545,6 +1549,8 @@ const koKR = { realTimeCollaboration: "실시간 협업", agentResourcesOverview: "에이전트 리소스 개요", skills: "스킬", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "사용자 지정 Agents 및 팀", workspaceGovernance: "Workspace 거버넌스", recurringJobs: "반복 작업", diff --git a/packages/docs/app/i18n/pt-BR.ts b/packages/docs/app/i18n/pt-BR.ts index 2f5dd807cd..4ce9efd704 100644 --- a/packages/docs/app/i18n/pt-BR.ts +++ b/packages/docs/app/i18n/pt-BR.ts @@ -1515,6 +1515,10 @@ const ptBR = { pureAgentApps: "Apps focados em automação", faq: "FAQ", server: "Servidor", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "Cliente", routing: "Roteamento", actions: "Ações", @@ -1548,6 +1552,8 @@ const ptBR = { realTimeCollaboration: "Colaboração em tempo real", agentResourcesOverview: "Visão geral dos recursos do agente", skills: "Habilidades", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "Agents e equipes personalizados", workspaceGovernance: "Governança do workspace", recurringJobs: "Jobs recorrentes", diff --git a/packages/docs/app/i18n/zh-CN.ts b/packages/docs/app/i18n/zh-CN.ts index 27da19bf14..f3c2b1d4a2 100644 --- a/packages/docs/app/i18n/zh-CN.ts +++ b/packages/docs/app/i18n/zh-CN.ts @@ -1492,6 +1492,10 @@ const zhCN = { pureAgentApps: "自动化优先应用", faq: "常见问题", server: "服务器", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "客户端", routing: "路由", actions: "行动", @@ -1525,6 +1529,8 @@ const zhCN = { realTimeCollaboration: "实时协作", agentResourcesOverview: "代理资源概览", skills: "技能", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "自定义 Agents 与团队", workspaceGovernance: "工作区治理", recurringJobs: "定期任务", diff --git a/packages/docs/app/i18n/zh-TW.ts b/packages/docs/app/i18n/zh-TW.ts index 42bcf57854..a6cd52f1d3 100644 --- a/packages/docs/app/i18n/zh-TW.ts +++ b/packages/docs/app/i18n/zh-TW.ts @@ -1490,6 +1490,10 @@ const messages = { pureAgentApps: "自動化優先應用程式", faq: "常見問題", server: "伺服器", + serverOverview: "Overview", + serverMiddleware: "Middleware", + serverPlugins: "Plugins", + serverRoutes: "Routes", client: "用戶端", routing: "路由", actions: "行動", @@ -1523,6 +1527,8 @@ const messages = { realTimeCollaboration: "即時協作", agentResourcesOverview: "代理資源概覽", skills: "技能", + agents: "Agents", + agentsOverview: "Overview", customAgentsTeams: "自訂 Agents 與團隊", workspaceGovernance: "工作區治理", recurringJobs: "定期工作", diff --git a/scripts/i18n-localized-doc-coverage-baseline.txt b/scripts/i18n-localized-doc-coverage-baseline.txt index 2349da029c..6d63d6c076 100644 --- a/scripts/i18n-localized-doc-coverage-baseline.txt +++ b/scripts/i18n-localized-doc-coverage-baseline.txt @@ -1,6 +1,7 @@ # Existing localized documentation files missing for English source docs. # Keep this file sorted. Remove entries as localized coverage improves. # Format: locale|relative/doc-slug +ar-SA|agents ar-SA|automation-connectors ar-SA|docs-components ar-SA|durable-background-runs @@ -14,6 +15,11 @@ ar-SA|integrations ar-SA|messaging-internals ar-SA|messaging-recipes ar-SA|organizations-teams-permissions +ar-SA|server-database +ar-SA|server-middleware +ar-SA|server-overview +ar-SA|server-plugins +ar-SA|server-routes ar-SA|template-analytics-connectors ar-SA|template-analytics-dashboards ar-SA|template-analytics-developers @@ -74,6 +80,7 @@ ar-SA|toolkit-settings ar-SA|toolkit-setup-connections ar-SA|toolkit-sharing ar-SA|toolkit-ui +de-DE|agents de-DE|automation-connectors de-DE|docs-components de-DE|durable-background-runs @@ -87,6 +94,11 @@ de-DE|integrations de-DE|messaging-internals de-DE|messaging-recipes de-DE|organizations-teams-permissions +de-DE|server-database +de-DE|server-middleware +de-DE|server-overview +de-DE|server-plugins +de-DE|server-routes de-DE|template-analytics-connectors de-DE|template-analytics-dashboards de-DE|template-analytics-developers @@ -147,6 +159,7 @@ de-DE|toolkit-settings de-DE|toolkit-setup-connections de-DE|toolkit-sharing de-DE|toolkit-ui +es-ES|agents es-ES|automation-connectors es-ES|docs-components es-ES|durable-background-runs @@ -160,6 +173,11 @@ es-ES|integrations es-ES|messaging-internals es-ES|messaging-recipes es-ES|organizations-teams-permissions +es-ES|server-database +es-ES|server-middleware +es-ES|server-overview +es-ES|server-plugins +es-ES|server-routes es-ES|template-analytics-connectors es-ES|template-analytics-dashboards es-ES|template-analytics-developers @@ -220,6 +238,7 @@ es-ES|toolkit-settings es-ES|toolkit-setup-connections es-ES|toolkit-sharing es-ES|toolkit-ui +fr-FR|agents fr-FR|automation-connectors fr-FR|docs-components fr-FR|durable-background-runs @@ -233,6 +252,11 @@ fr-FR|integrations fr-FR|messaging-internals fr-FR|messaging-recipes fr-FR|organizations-teams-permissions +fr-FR|server-database +fr-FR|server-middleware +fr-FR|server-overview +fr-FR|server-plugins +fr-FR|server-routes fr-FR|template-analytics-connectors fr-FR|template-analytics-dashboards fr-FR|template-analytics-developers @@ -293,6 +317,7 @@ fr-FR|toolkit-settings fr-FR|toolkit-setup-connections fr-FR|toolkit-sharing fr-FR|toolkit-ui +hi-IN|agents hi-IN|automation-connectors hi-IN|docs-components hi-IN|durable-background-runs @@ -306,6 +331,11 @@ hi-IN|integrations hi-IN|messaging-internals hi-IN|messaging-recipes hi-IN|organizations-teams-permissions +hi-IN|server-database +hi-IN|server-middleware +hi-IN|server-overview +hi-IN|server-plugins +hi-IN|server-routes hi-IN|template-analytics-connectors hi-IN|template-analytics-dashboards hi-IN|template-analytics-developers @@ -366,6 +396,7 @@ hi-IN|toolkit-settings hi-IN|toolkit-setup-connections hi-IN|toolkit-sharing hi-IN|toolkit-ui +ja-JP|agents ja-JP|automation-connectors ja-JP|docs-components ja-JP|durable-background-runs @@ -379,6 +410,11 @@ ja-JP|integrations ja-JP|messaging-internals ja-JP|messaging-recipes ja-JP|organizations-teams-permissions +ja-JP|server-database +ja-JP|server-middleware +ja-JP|server-overview +ja-JP|server-plugins +ja-JP|server-routes ja-JP|template-analytics-connectors ja-JP|template-analytics-dashboards ja-JP|template-analytics-developers @@ -439,6 +475,7 @@ ja-JP|toolkit-settings ja-JP|toolkit-setup-connections ja-JP|toolkit-sharing ja-JP|toolkit-ui +ko-KR|agents ko-KR|automation-connectors ko-KR|docs-components ko-KR|durable-background-runs @@ -452,6 +489,11 @@ ko-KR|integrations ko-KR|messaging-internals ko-KR|messaging-recipes ko-KR|organizations-teams-permissions +ko-KR|server-database +ko-KR|server-middleware +ko-KR|server-overview +ko-KR|server-plugins +ko-KR|server-routes ko-KR|template-analytics-connectors ko-KR|template-analytics-dashboards ko-KR|template-analytics-developers @@ -512,6 +554,7 @@ ko-KR|toolkit-settings ko-KR|toolkit-setup-connections ko-KR|toolkit-sharing ko-KR|toolkit-ui +pt-BR|agents pt-BR|automation-connectors pt-BR|docs-components pt-BR|durable-background-runs @@ -525,6 +568,11 @@ pt-BR|integrations pt-BR|messaging-internals pt-BR|messaging-recipes pt-BR|organizations-teams-permissions +pt-BR|server-database +pt-BR|server-middleware +pt-BR|server-overview +pt-BR|server-plugins +pt-BR|server-routes pt-BR|template-analytics-connectors pt-BR|template-analytics-dashboards pt-BR|template-analytics-developers @@ -585,6 +633,7 @@ pt-BR|toolkit-settings pt-BR|toolkit-setup-connections pt-BR|toolkit-sharing pt-BR|toolkit-ui +zh-CN|agents zh-CN|automation-connectors zh-CN|docs-components zh-CN|durable-background-runs @@ -598,6 +647,11 @@ zh-CN|integrations zh-CN|messaging-internals zh-CN|messaging-recipes zh-CN|organizations-teams-permissions +zh-CN|server-database +zh-CN|server-middleware +zh-CN|server-overview +zh-CN|server-plugins +zh-CN|server-routes zh-CN|template-analytics-connectors zh-CN|template-analytics-dashboards zh-CN|template-analytics-developers @@ -658,6 +712,7 @@ zh-CN|toolkit-settings zh-CN|toolkit-setup-connections zh-CN|toolkit-sharing zh-CN|toolkit-ui +zh-TW|agents zh-TW|automation-connectors zh-TW|docs-components zh-TW|external-agents-catalog @@ -670,6 +725,11 @@ zh-TW|integrations zh-TW|messaging-internals zh-TW|messaging-recipes zh-TW|organizations-teams-permissions +zh-TW|server-database +zh-TW|server-middleware +zh-TW|server-overview +zh-TW|server-plugins +zh-TW|server-routes zh-TW|template-analytics-connectors zh-TW|template-analytics-dashboards zh-TW|template-analytics-developers From bfdea212a82356dd185d47b2c19caac80f198024 Mon Sep 17 00:00:00 2001 From: Wes Reid Date: Tue, 4 Aug 2026 13:50:41 -0700 Subject: [PATCH 2/6] Improve draft server-plugins doc Reframes db.ts as optional rather than default, moves the database plugin section after auth and agent-chat, expands the intro to explain what plugins are and how Nitro discovers them, and adds a new section on writing custom plugins with defineNitroPlugin linking to Nitro docs. --- packages/core/docs/content/server-plugins.mdx | 111 ++++++++++++------ 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/packages/core/docs/content/server-plugins.mdx b/packages/core/docs/content/server-plugins.mdx index 33b8a5254c..089998ce02 100644 --- a/packages/core/docs/content/server-plugins.mdx +++ b/packages/core/docs/content/server-plugins.mdx @@ -1,51 +1,30 @@ --- title: "Plugins" -description: "The three startup plugins every agent-native app ships: database migrations, auth configuration, and the agent-chat plugin." +description: "Startup plugins in server/plugins/ — what they are, the two plugins every app ships, the optional database plugin, and how to write your own." draft: true --- # Plugins -Plugins live in `server/plugins/` and run at server startup, before any routes are served. Every app ships three: one that runs database migrations, one that configures authentication, and one that mounts the agent chat loop with your app's actions and system prompt. +Plugins are startup hooks. Each file in `server/plugins/` exports a function that Nitro calls once when the server initializes, before any route is served. They are the right place for one-time setup: configuring auth, mounting the agent loop, running database migrations, or registering any other initialization code that needs to complete before requests arrive. + +Nitro discovers plugins automatically from the filesystem. No registration or imports are required. Files run in alphabetical order, so execution order is controlled by filename prefix. + +Every agent-native app ships two plugins by default: ```text server/plugins/ - db.ts -> run migrations, create tables auth.ts -> configure auth provider and public paths - agent-chat.ts -> mount the agent with your actions and system prompt + agent-chat.ts -> mount the agent loop with your actions and system prompt ``` -## Database Plugin {#database-plugin} +Apps that need custom database tables add a third: -`server/plugins/db.ts` runs schema migrations on startup using `runMigrations` from `@agent-native/core/db`. Migrations must be additive — never drop, rename, or destructively alter tables here. - -```ts filename="server/plugins/db.ts" -import { runMigrations } from "@agent-native/core/db"; - -export default runMigrations( - [ - { - version: 1, - sql: `CREATE TABLE IF NOT EXISTS projects ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - owner_email TEXT NOT NULL, - org_id TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - )`, - }, - ], - { table: "my_app_migrations" }, -); +```text +server/plugins/ + db.ts -> run migrations on startup ``` -Each entry needs a `version` number and a `sql` string. The `table` option names the migration tracking table — use a name unique to your app so it doesn't collide with framework migrations. See [Database](/docs/database) for the full schema authoring guide. - - - Migrations must be additive. Never put destructive SQL (`DROP TABLE`, - `ALTER TABLE ... DROP COLUMN`, `DELETE FROM`) in startup plugins. - - ## Auth Plugin {#auth-plugin} `server/plugins/auth.ts` configures how users sign in. `createAuthPlugin` from `@agent-native/core/server` wires up the auth provider and sets the sign-in page branding: @@ -75,7 +54,7 @@ export default createAuthPlugin({ }); ``` -The auth plugin configures the guard; the auth middleware in `server/middleware/auth.ts` enforces it on every request. See [Middleware](/docs/server-middleware). +The auth plugin configures the guard. The middleware in `server/middleware/auth.ts` enforces it on every request. See [Middleware](/docs/server-middleware). ## Agent-Chat Plugin {#agent-chat-plugin} @@ -102,19 +81,73 @@ screen when context matters.`, }); ``` -`loadActionsFromStaticRegistry` picks up every action exported from `actions/` through the generated registry. `initialToolNames` controls which actions the agent has access to at the start of a conversation — it can still call others as needed. +`loadActionsFromStaticRegistry` picks up every action exported from `actions/` through the generated registry. `initialToolNames` controls which actions the agent has access to at the start of a conversation. It can still call others as needed. + +## Database Plugin {#database-plugin} + +If your app defines custom database tables, add `server/plugins/db.ts` to run schema migrations at startup. It uses `runMigrations` from `@agent-native/core/db`: + +```ts filename="server/plugins/db.ts" +import { runMigrations } from "@agent-native/core/db"; + +export default runMigrations( + [ + { + version: 1, + sql: `CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + owner_email TEXT NOT NULL, + org_id TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + )`, + }, + ], + { table: "my_app_migrations" }, +); +``` + +Each entry needs a `version` number and a `sql` string. The `table` option names the migration tracking table. Use a name unique to your app so it does not collide with framework migrations. See [Database](/docs/server-database) for the full schema authoring guide. + + + Migrations must be additive. Never put destructive SQL (`DROP TABLE`, + `ALTER TABLE ... DROP COLUMN`, `DELETE FROM`) in startup plugins. + + +## Writing Custom Plugins {#custom-plugins} + +The framework helpers (`createAuthPlugin`, `createAgentChatPlugin`, `runMigrations`) each return a Nitro plugin function. For anything outside those helpers, write a plugin using `defineNitroPlugin`: + +```ts filename="server/plugins/my-plugin.ts" +import { defineNitroPlugin } from "nitropack/runtime"; + +export default defineNitroPlugin((nitro) => { + nitro.hooks.hook("request", (event) => { + // runs on every request + }); + + nitro.hooks.hookOnce("close", async () => { + // runs when the server shuts down + }); +}); +``` + +Plugins can use any of Nitro's lifecycle hooks: `request`, `beforeResponse`, `close`, and others. The [Nitro plugin docs](https://nitro.build/guide/plugins) cover the full hook API and the plugin contract in detail. ## Startup Order {#startup-order} -Nitro runs plugins in filesystem order. Within a single plugin file the default export is awaited before the next plugin starts. The typical order is: +Nitro runs plugins in alphabetical filename order. Within a single plugin file the default export is awaited before the next plugin starts. A typical ordering: 1. `db.ts` — migrations complete before any route or plugin reads the database 2. `auth.ts` — auth configuration is in place before requests arrive 3. `agent-chat.ts` — agent routes mount after auth is configured +If you add custom plugins, prefix their filenames to control where they fall in the sequence. + ## What's next -- [**Database**](/docs/database) — schema helpers and the `getDb` pattern used inside plugins -- [**Middleware**](/docs/server-middleware) — the auth guard that enforces what the auth plugin configures -- [**Routes**](/docs/server-routes) — custom file routes served after plugins complete -- [**Writing Agent Instructions**](/docs/writing-agent-instructions) — how to craft the `systemPrompt` in the agent-chat plugin +- [**Database**](/docs/server-database): schema helpers and the `getDb` pattern used inside plugins +- [**Middleware**](/docs/server-middleware): the auth guard that enforces what the auth plugin configures +- [**Routes**](/docs/server-routes): custom file routes served after plugins complete +- [**Writing Agent Instructions**](/docs/writing-agent-instructions): how to craft the `systemPrompt` in the agent-chat plugin +- [**Nitro plugin docs**](https://nitro.build/guide/plugins): the full plugin API and lifecycle hooks From 1e341c81d9d2b4ecb75c0ae032e4f28ce0e521b0 Mon Sep 17 00:00:00 2001 From: Wes Reid Date: Tue, 4 Aug 2026 13:58:04 -0700 Subject: [PATCH 3/6] Docs: rewrite server-middleware draft with diagram and custom middleware guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expands the draft from a stub into a full page: - Adds a diagram showing callers → middleware → allowed/blocked outcomes - Rewrites the intro to describe middleware as a general cross-cutting layer - Adds a "Writing Middleware" section with a defineEventHandler example and links to Nitro/H3 docs, placed before the auth section - Consolidates The Auth Guard, Why Middleware, and Adding Public Paths under a single "The Auth Middleware" H2 with those as H3 subheadings --- .../core/docs/content/server-middleware.mdx | 121 ++++++++++++++++-- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/packages/core/docs/content/server-middleware.mdx b/packages/core/docs/content/server-middleware.mdx index a60972d762..adc391eca7 100644 --- a/packages/core/docs/content/server-middleware.mdx +++ b/packages/core/docs/content/server-middleware.mdx @@ -1,16 +1,114 @@ --- title: "Middleware" -description: "Nitro middleware in server/middleware/ — what it is, how the auth guard works, and why it lives there instead of in a plugin or route." +description: "Nitro middleware in server/middleware/ — what it is, how to write your own, and how the built-in auth guard works." draft: true --- # Middleware -Nitro middleware runs on every incoming request before it reaches a route handler. Files in `server/middleware/` are picked up automatically — no registration needed. +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. + + + +```html +
+
+
Browser / UI
+
Agent loop
+
+ External clients
HTTP · MCP · A2A +
+
+ +
+ Middleware + server/middleware/ + runs on every request +
+
+
+ allowed + +
Routes & handlers
+
+
+ blocked + +
Redirect / error
+
+
+
+``` + +```css +.diagram-mw { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} +.diagram-mw .diagram-col { + display: flex; + flex-direction: column; + gap: 10px; +} +.diagram-mw .diagram-panel { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 16px; +} +.diagram-mw .diagram-arrow { + font-size: 22px; + line-height: 1; +} +.mw-outcomes { + display: flex; + flex-direction: column; + gap: 10px; +} +.mw-outcome { + display: flex; + align-items: center; + gap: 8px; +} +.mw-label { + font-size: 11px; + min-width: 44px; + text-align: right; +} +.mw-arrow { + font-size: 16px; + line-height: 1; +} +``` + +
+ +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. + +## Writing Middleware {#writing-middleware} + +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`. + +```ts filename="server/middleware/cors.ts" +import { defineEventHandler, setResponseHeader } from "h3"; + +export default defineEventHandler((event) => { + setResponseHeader(event, "Access-Control-Allow-Origin", "*"); + // returning nothing passes the request through to the route handler +}); +``` + +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. + +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). + +## The Auth Middleware {#auth-middleware} -Every agent-native app ships one middleware file: `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. +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. -## The Auth Guard {#auth-guard} +### The Auth Guard {#auth-guard} ```ts filename="server/middleware/auth.ts" import { runAuthGuard } from "@agent-native/core/server"; @@ -23,13 +121,13 @@ export default defineEventHandler(async (event) => { `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. -## Why Middleware, Not a Plugin or Route Guard {#why-middleware} +### Why Middleware, Not a Plugin or Route Guard {#why-middleware} -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 — only framework routes would be protected. +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. -Plugins run at startup, not per-request, so they can't 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. +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. -## Adding Public Paths {#public-paths} +### Adding Public Paths {#public-paths} To allow unauthenticated access to specific paths, pass them to `createAuthPlugin` in `server/plugins/auth.ts` — not to the middleware directly: @@ -46,6 +144,7 @@ export default createAuthPlugin({ ## What's next -- [**Plugins**](/docs/server-plugins) — auth plugin configuration that the middleware enforces -- [**Routes**](/docs/server-routes) — custom routes that are protected by this middleware -- [**Security**](/docs/security) — data scoping, access guards, and the full auth model +- [**Plugins**](/docs/server-plugins): auth plugin configuration that the middleware enforces +- [**Routes**](/docs/server-routes): custom routes that are protected by this middleware +- [**Security**](/docs/security): data scoping, access guards, and the full auth model +- [**Nitro middleware docs**](https://nitro.build/guide/routing#middleware): full middleware API and patterns From 574c120eab4eba1c7cae87f36cbd849f087dba3d Mon Sep 17 00:00:00 2001 From: Wes Reid Date: Tue, 4 Aug 2026 14:31:37 -0700 Subject: [PATCH 4/6] Docs: rewrite server-database draft with clearer structure and new diagram Reorganizes the draft from a loosely ordered reference into a narrative that builds from concept to implementation: - New diagram showing Browser/UI and Agent loop reaching the database through the same Actions layer - Hosting Options section covering all five backends in order of complexity (SQLite default, PGlite, Postgres, Turso, Builder managed) - Setting Up section as four numbered steps: define schema, create DB client, write migrations, query in actions - Scoping Data to Users consolidates the owner_email and ownableColumns patterns that were previously scattered - Removes all em-dashes throughout in favor of separate sentences --- .../core/docs/content/server-database.mdx | 334 ++++++++++-------- 1 file changed, 191 insertions(+), 143 deletions(-) diff --git a/packages/core/docs/content/server-database.mdx b/packages/core/docs/content/server-database.mdx index fddcf6cc1e..dd90094a7c 100644 --- a/packages/core/docs/content/server-database.mdx +++ b/packages/core/docs/content/server-database.mdx @@ -1,93 +1,89 @@ --- title: "Database" -description: "Connect a portable SQL database to your agent-native app and write provider-agnostic Drizzle code." +description: "Connect a portable SQL database to your agent-native app: schema helpers, migrations, and the hosted database options." draft: true --- # Database -Agent-native apps use [Drizzle ORM](https://orm.drizzle.team) and support portable SQL backends. For anything beyond local development, connect a persistent SQL database — Postgres, libSQL/Turso, or another Drizzle-compatible backend — by setting `DATABASE_URL`. When that variable is unset, the app falls back to a zero-config local SQLite file so you can start developing immediately. For local development that should behave like Postgres without running a separate database server, opt into PGlite with `DATABASE_URL=pglite:./data/pglite`. +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. - + ```html -
-
- @agent-native/core/db/schematable · text · integer · real · now+ Drizzle query DSL +
+
+
Browser / UI
+
Agent loop
-
- DATABASE_URL
dialect auto-detected +
+ Actions + defineAction + getDb() · Drizzle ORM
-
- Postgres
Neon · Supabase
libSQL / TursoCloudflare D1SQLite file
unset = local dev only
PGlite
local Postgres opt-in
+ SQL database
dialect from DATABASE_URL
``` ```css -.diagram-db { +.diagram-dbr { display: flex; align-items: center; - gap: 12px; + gap: 14px; flex-wrap: wrap; } -.diagram-db .center { +.diagram-dbr .diagram-col { + display: flex; + flex-direction: column; + gap: 10px; +} +.diagram-dbr .diagram-panel { display: flex; flex-direction: column; - align-items: center; gap: 4px; - padding: 14px 16px; + padding: 12px 16px; } -.diagram-db .diagram-arrow { +.diagram-dbr .diagram-arrow { font-size: 22px; line-height: 1; } -.diagram-db .diagram-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} ``` -## Local default: SQLite file {#default-sqlite} +## Hosting Options {#hosting} -When `DATABASE_URL` is not set, the app creates a SQLite database at `data/app.db`. This is the zero-config default for local development — no setup required. It is meant for development only; for production, set `DATABASE_URL` to a persistent SQL database. +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. -Do not rely on that local file for deployed apps. Containers, serverless functions, and preview environments may reset their filesystem, which means a local SQLite file can disappear between restarts. Set `DATABASE_URL` to a persistent hosted database before production use. +### Default: SQLite file {#default-sqlite} -## Local Postgres Opt-In: PGlite {#local-pglite} +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. -Install the optional PGlite package, then set `DATABASE_URL=pglite:./data/pglite` to run the app against [PGlite](https://pglite.dev/), a local WASM Postgres database: +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. + +### Local Postgres: PGlite {#local-pglite} + +To develop locally against the Postgres dialect without Docker or a hosted database, install the optional PGlite package and set `DATABASE_URL`: ```bash pnpm add @electric-sql/pglite@^0.5.3 ``` -This keeps local development on the Postgres dialect, including Postgres schema helpers and migrations, without requiring Docker or a hosted database. +```bash +DATABASE_URL=pglite:./data/pglite +``` -PGlite is still local development storage. Treat it like the SQLite fallback for durability and sharing: use it to test Postgres-shaped behavior on your machine, then set `DATABASE_URL` to a persistent hosted database for production, previews, or any shared environment. +[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. -## Connecting a Production Database {#production} +### Production database {#production} -Set `DATABASE_URL` in your `.env` file or deploy-provider environment to connect a hosted database. Turso is not required; use whichever Drizzle-compatible SQL backend fits your deployment: +Set `DATABASE_URL` in your `.env` file or deploy-provider environment to connect a hosted database: ```bash # Neon Postgres @@ -99,62 +95,48 @@ DATABASE_URL=postgres://postgres.xxxx:pass@aws-0-us-east-1.pooler.supabase.com:6 # Plain Postgres DATABASE_URL=postgres://user:pass@localhost:5432/mydb -# Local PGlite (Postgres dialect, local development only) -DATABASE_URL=pglite:./data/pglite - -# Turso (libSQL) +# Turso (libSQL). Also requires DATABASE_AUTH_TOKEN. DATABASE_URL=libsql://my-db-org.turso.io -DATABASE_AUTH_TOKEN=your-token ``` -The framework auto-detects the dialect from the URL and configures Drizzle accordingly. The built-in adapters cover Postgres URLs, local PGlite URLs, libSQL/Turso URLs, SQLite file URLs, and Cloudflare D1 bindings. Common production choices include Neon, Supabase, Turso/libSQL, plain Postgres, durable SQLite, and Builder.io-managed environments when available. +The framework auto-detects the dialect from the URL prefix and configures Drizzle accordingly. -## Builder.io Managed Database {#builder-managed} +### Builder.io managed database {#builder-managed} _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. -## Where the DB Client Lives {#db-client} +## Setting Up the Database {#setup} -Each template creates a lazy, singleton Drizzle client by calling `createGetDb(schema)` from `@agent-native/core/db`. The canonical location is `server/db/index.ts`: +An app that uses the database needs three files: -```ts filename="server/db/index.ts" -import { createGetDb } from "@agent-native/core/db"; -import * as schema from "./schema.js"; - -export const getDb = createGetDb(schema); -``` +- `server/db/schema.ts`: table definitions +- `server/db/index.ts`: the typed DB client singleton +- `server/plugins/db.ts`: migrations that run at startup -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. +### 1. Define your schema {#schema} -## Dialect-Agnostic Schema And Queries {#schema} +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. -App database code should use Drizzle's schema and query DSL so it can run across providers. Never write SQLite-only syntax (`INSERT OR REPLACE`, `AUTOINCREMENT`, `datetime('now')`) or Postgres-only syntax in product code. - -Use the framework's schema helpers from `@agent-native/core/db/schema`: - -```ts -import { table, text, integer, real, now } from "@agent-native/core/db/schema"; +```ts filename="server/db/schema.ts" +import { integer, now, table, text } from "@agent-native/core/db/schema"; export const tasks = table("tasks", { id: text("id").primaryKey(), title: text("title").notNull(), priority: integer("priority").notNull().default(0), - weight: real("weight"), done: integer("done", { mode: "boolean" }).notNull().default(false), ownerEmail: text("owner_email").notNull(), createdAt: text("created_at").notNull().default(now()), }); ``` -| Helper | Purpose | -| --------- | --------------------------------------------------------------- | -| `table` | Define a table — delegates to `pgTable` or `sqliteTable` | -| `text` | Text column, supports `{ enum: [...] }` | -| `integer` | Integer column, `{ mode: "boolean" }` maps to Postgres boolean | -| `real` | Float column — `real` on SQLite, `double precision` on Postgres | -| `now` | Dialect-agnostic current timestamp for `.default(now())` | - -The `tasks` table above defines the same columns on every backend: +| Helper | Purpose | +| --------- | ---------------------------------------------------------------------- | +| `table` | Define a table; dispatches to `pgTable` or `sqliteTable` | +| `text` | Text column, supports `{ enum: [...] }` | +| `integer` | Integer column, `{ mode: "boolean" }` maps to Postgres boolean | +| `real` | Float column. Maps to `real` on SQLite, `double precision` on Postgres | +| `now` | Dialect-agnostic current timestamp for `.default(now())` | -Never import from `drizzle-orm/sqlite-core` or `drizzle-orm/pg-core` directly. Always use `@agent-native/core/db/schema`. +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. -Tables that store user-facing data must include an `owner_email` column so the framework's SQL-level scoping can filter rows to the authenticated user — see [Security](/docs/security#data-scoping). 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 [Sharing](/docs/sharing#building). +### 2. Create the DB client {#db-client} -For reads and writes, use Drizzle's query builder and portable operators from `drizzle-orm`: +Each app creates a lazy, singleton Drizzle client by calling `createGetDb(schema)`. The canonical location is `server/db/index.ts`: + +```ts filename="server/db/index.ts" +import { createGetDb } from "@agent-native/core/db"; +import * as schema from "./schema.js"; + +export const getDb = createGetDb(schema); +``` + +`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. + +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. + +### 3. Write migrations {#migrations} + +Schema changes are applied at startup via a Nitro plugin in `server/plugins/db.ts`. Use `runMigrations` from `@agent-native/core/db`: + + + +`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. + + + Never run `drizzle-kit push` against a production database. Template schemas + only define app-specific tables; they do not include central framework tables + (`user`, `session`, `application_state`, and others). Running `drizzle-kit + push` against production will detect those tables as unknown and attempt to + drop them, causing immediate data loss. + + +`drizzle.config.ts` at the root of each app configures drizzle-kit for local development schema inspection: + +```ts filename="drizzle.config.ts" +import { createDrizzleConfig } from "@agent-native/core/db/drizzle-config"; +export default createDrizzleConfig(); +``` + +Use `pnpm db:generate` to inspect your schema and `pnpm db:push` against a local database only. + +### 4. Query in actions {#querying} + +Call `getDb()` from your actions to get the typed Drizzle client. Use Drizzle's query builder and portable operators from `drizzle-orm`: ```ts import { and, desc, eq } from "drizzle-orm"; import { getDb } from "../server/db/index.js"; -import { tasks } from "../server/db/schema.js"; +import * as schema from "../server/db/schema.js"; const db = getDb(); const openTasks = await db .select() - .from(tasks) - .where(and(eq(tasks.ownerEmail, userEmail), eq(tasks.done, false))) - .orderBy(desc(tasks.createdAt)); + .from(schema.tasks) + .where( + and(eq(schema.tasks.ownerEmail, userEmail), eq(schema.tasks.done, false)), + ) + .orderBy(desc(schema.tasks.createdAt)); + +await db + .update(schema.tasks) + .set({ done: true }) + .where(eq(schema.tasks.id, taskId)); +``` + +## Scoping Data to Users {#scoping} + +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. -await db.update(tasks).set({ done: true }).where(eq(tasks.id, taskId)); +**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: + +```ts +.where(eq(schema.tasks.ownerEmail, userEmail)) ``` -## Raw SQL Escape Hatches {#raw-sql} +**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`: -Raw SQL is not the default app-code API. Use it only for additive migrations, health checks, carefully reviewed advanced queries that Drizzle cannot express, or one-off maintenance. Keep it parameterized and dialect-agnostic. For timestamps in Drizzle schemas, prefer `.default(now())`; for migration SQL, use `runMigrations()` so framework-supported compatibility rewrites and dialect-gated statements stay centralized. +```ts filename="server/db/schema.ts" +import { + table, + text, + ownableColumns, + createSharesTable, +} from "@agent-native/core/db/schema"; -For cases where you truly need raw SQL outside of Drizzle queries: +export const decks = table("decks", { + id: text("id").primaryKey(), + title: text("title").notNull(), + ...ownableColumns(), +}); +export const deckShares = createSharesTable("deck_shares"); +``` -- `getDbExec()` — auto-converts `?` params to `$1` for Postgres -- `isPostgres()` — runtime dialect check -- `intType()` — returns the correct integer type for the current dialect +Then use `accessFilter` from `@agent-native/core/sharing` in list queries instead of a manual `eq` check: -## Migrations and Schema Updates {#migrations} +```ts +import { accessFilter } from "@agent-native/core/sharing"; -In hosted environments, multiple deployment previews, branches, and the production server share the same underlying database. Therefore, database schema updates must follow strict constraints to avoid data loss and service disruption. +const rows = await db + .select() + .from(schema.decks) + .where(accessFilter(schema.decks, schema.deckShares)); +``` -### The "Zero Destructive Changes" Rule +`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. -All database schema updates must be **strictly additive**. +See [Security — Data Scoping](/docs/security#data-scoping) and [Sharing](/docs/sharing#building) for the full model. -- **Do not drop tables or columns.** -- **Do not rename tables or columns.** Renaming a column or table looks like a drop + create sequence to Drizzle, which will permanently delete your existing production data. -- If a column needs to be renamed or replaced, add the new column alongside the old one, update your application code to read from/write to both, migrate the data, and only retire the old column in a later release once no active deployments are referencing it. +## Raw SQL {#raw-sql} - - **Never run `drizzle-kit push` against a production database.** Template - database schemas only define app-specific domain tables; they do not define - central framework tables (`user`, `session`, `application_state`, etc.). If - you run `drizzle-kit push` against production, Drizzle will detect these - framework tables as "not in schema" and attempt to drop them, causing - immediate system-wide failure and data loss. - +For advanced queries, health checks, or one-off maintenance that the Drizzle query builder can't express, use `getDbExec` from `@agent-native/core/db`: -### Safe Migration Path +```ts +import { getDbExec, isPostgres, intType } from "@agent-native/core/db"; -Instead of pushing directly, schema changes should be applied via SQL migrations executed at application startup. Implement additive migrations within a server plugin (e.g., `server/plugins/db.ts`) by invoking the framework's `runMigrations()` helper: +const { rows } = await getDbExec().execute({ + sql: `SELECT id, title FROM tasks WHERE owner_email = ? LIMIT ?`, + args: [userEmail, 50], +}); +``` - +`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. ## Environment Variables {#environment-variables} @@ -306,8 +354,8 @@ Instead of pushing directly, schema changes should be applied via SQL migrations ## What's next -- [**Security — Data Scoping**](/docs/security#data-scoping) — how `owner_email` and access helpers scope reads and writes -- [**Sharing**](/docs/sharing#building) — `ownableColumns()` and the visibility model for shared resources -- [**Server**](/docs/server#request-context) — how actions and custom routes each import `getDb` -- [**Deployment**](/docs/deployment#persistent-database) — connecting a persistent database per deploy target -- [**Actions**](/docs/actions#access-control) — a complete, paste-ready action that reads and writes through `getDb`/`schema` +- [**Security — Data Scoping**](/docs/security#data-scoping): how `owner_email` and access helpers scope reads and writes +- [**Sharing**](/docs/sharing#building): `ownableColumns()` and the visibility model for shared resources +- [**Plugins**](/docs/server-plugins): the startup plugin lifecycle where migrations run +- [**Actions**](/docs/actions): the surface where actions call `getDb()` to read and write data +- [**Deployment**](/docs/deployment#persistent-database): connecting a persistent database per deploy target From a156a05b1dab1e8f32328b03243790141c717802 Mon Sep 17 00:00:00 2001 From: Wes Reid Date: Tue, 4 Aug 2026 14:46:23 -0700 Subject: [PATCH 5/6] Docs: add SQL-Backed Sync section to server-database draft Ports the sync loop content from the published server.mdx into the database page, where it completes the write story: after an action mutates data, the sync version increments and useDbSync() on the client invalidates caches so the UI refreshes. Includes the sync loop diagram and the poll endpoint block. Adds a What's next link to real-time-sync. --- .../core/docs/content/server-database.mdx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/core/docs/content/server-database.mdx b/packages/core/docs/content/server-database.mdx index dd90094a7c..b541cdb36a 100644 --- a/packages/core/docs/content/server-database.mdx +++ b/packages/core/docs/content/server-database.mdx @@ -330,6 +330,68 @@ const rows = await db See [Security — Data Scoping](/docs/security#data-scoping) and [Sharing](/docs/sharing#building) for the full model. +## SQL-Backed Sync {#sync} + +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. + +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. + + + +```html +
+
+ Action / helper
mutates data +
+ +
+ SQL databasesync version increments +
+ +
+
+ useDbSync()
polls /_agent-native/poll +
+
invalidate caches → UI refreshes
+
+
+``` + +```css +.diagram-db-sync { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} +.diagram-db-sync .diagram-col { + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-start; +} +.diagram-db-sync .diagram-arrow { + font-size: 22px; + line-height: 1; +} +``` + +
+ + + +`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. + + + ## Raw SQL {#raw-sql} For advanced queries, health checks, or one-off maintenance that the Drizzle query builder can't express, use `getDbExec` from `@agent-native/core/db`: @@ -359,3 +421,4 @@ const { rows } = await getDbExec().execute({ - [**Plugins**](/docs/server-plugins): the startup plugin lifecycle where migrations run - [**Actions**](/docs/actions): the surface where actions call `getDb()` to read and write data - [**Deployment**](/docs/deployment#persistent-database): connecting a persistent database per deploy target +- [**Real-Time Sync**](/docs/real-time-sync): `useDbSync()` and the full client-side sync model From a95bb672c44fdcc30c9380d405e7d96c8d348c90 Mon Sep 17 00:00:00 2001 From: Wes Reid Date: Tue, 4 Aug 2026 15:01:44 -0700 Subject: [PATCH 6/6] Docs: publish Server section, replacing the single server.mdx page Splits server.mdx into five pages (overview, database, middleware, plugins, routes), un-drafts them in the nav, and repoints cross-page links across the docs that pointed at the old combined page. --- packages/core/docs/content/actions.mdx | 2 +- packages/core/docs/content/agent-surfaces.mdx | 2 +- packages/core/docs/content/database.mdx | 4 +- packages/core/docs/content/drop-in-agent.mdx | 2 +- packages/core/docs/content/notifications.mdx | 2 +- packages/core/docs/content/routing.mdx | 2 +- .../core/docs/content/server-database.mdx | 1 - .../core/docs/content/server-middleware.mdx | 1 - .../core/docs/content/server-overview.mdx | 1 - packages/core/docs/content/server-plugins.mdx | 1 - packages/core/docs/content/server-routes.mdx | 1 - packages/core/docs/content/server.mdx | 469 ------------------ packages/docs/app/components/docsNavItems.ts | 7 - 13 files changed, 7 insertions(+), 488 deletions(-) delete mode 100644 packages/core/docs/content/server.mdx diff --git a/packages/core/docs/content/actions.mdx b/packages/core/docs/content/actions.mdx index f7a084da06..feeb01c0e9 100644 --- a/packages/core/docs/content/actions.mdx +++ b/packages/core/docs/content/actions.mdx @@ -90,7 +90,7 @@ page. For the full surface map, see [Agent Surfaces](/docs/agent-surfaces). If the UI and agent both need to do something, reach for an action — not a custom route. For when a route-shaped protocol _is_ the right call, see [Prefer Actions -For App Operations](/docs/server#actions-first). +For App Operations](/docs/server-overview#actions-first). On this page: [Start with one action](#hello-action), [Defining an action](#defining), [Run context](#run-context), [Access control](#access-control), diff --git a/packages/core/docs/content/agent-surfaces.mdx b/packages/core/docs/content/agent-surfaces.mdx index 009d9aff4c..ac4ba46504 100644 --- a/packages/core/docs/content/agent-surfaces.mdx +++ b/packages/core/docs/content/agent-surfaces.mdx @@ -263,7 +263,7 @@ await runAgentLoop({ For most apps, scheduled prompts and integration webhooks already call this loop for you. Reach for it directly only when building a custom no-browser host, eval runner, or server-side orchestration surface — see [Server — Production agent -handler](/docs/server#agent-handler) for the full signature. +handler](/docs/server-plugins#agent-chat-plugin) for the full signature. ### Running against a folder {#folder-loop} diff --git a/packages/core/docs/content/database.mdx b/packages/core/docs/content/database.mdx index 92d7b14745..ec40071c97 100644 --- a/packages/core/docs/content/database.mdx +++ b/packages/core/docs/content/database.mdx @@ -123,7 +123,7 @@ import * as schema from "./schema.js"; export const getDb = createGetDb(schema); ``` -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. +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. ## Dialect-Agnostic Schema And Queries {#schema} @@ -307,6 +307,6 @@ Instead of pushing directly, schema changes should be applied via SQL migrations - [**Security — Data Scoping**](/docs/security#data-scoping) — how `owner_email` and access helpers scope reads and writes - [**Sharing**](/docs/sharing#building) — `ownableColumns()` and the visibility model for shared resources -- [**Server**](/docs/server#request-context) — how actions and custom routes each import `getDb` +- [**Server**](/docs/server-routes#request-context) — how actions and custom routes each import `getDb` - [**Deployment**](/docs/deployment#persistent-database) — connecting a persistent database per deploy target - [**Actions**](/docs/actions#access-control) — a complete, paste-ready action that reads and writes through `getDb`/`schema` diff --git a/packages/core/docs/content/drop-in-agent.mdx b/packages/core/docs/content/drop-in-agent.mdx index c6761ac0fc..be52e67357 100644 --- a/packages/core/docs/content/drop-in-agent.mdx +++ b/packages/core/docs/content/drop-in-agent.mdx @@ -15,7 +15,7 @@ You don't need to build agent-native from scratch. The agent chat, resources tab -**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). +**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). diff --git a/packages/core/docs/content/notifications.mdx b/packages/core/docs/content/notifications.mdx index 257406063d..76a1db5088 100644 --- a/packages/core/docs/content/notifications.mdx +++ b/packages/core/docs/content/notifications.mdx @@ -374,4 +374,4 @@ Automations can chain off this — e.g. _"if a critical notification fires, also - [**Automations**](/docs/automations) — the most common caller of `notify()` - [**Security**](/docs/security) — the `${keys.NAME}` substitution that powers the webhook channel -- [**Server plugins**](/docs/server) — where custom channels are registered at startup +- [**Server plugins**](/docs/server-plugins) — where custom channels are registered at startup diff --git a/packages/core/docs/content/routing.mdx b/packages/core/docs/content/routing.mdx index 2d4d868edb..6360c6104c 100644 --- a/packages/core/docs/content/routing.mdx +++ b/packages/core/docs/content/routing.mdx @@ -113,5 +113,5 @@ navigate(`/inbox/${threadId}`); ## What's next - [**Client**](/docs/client) — the agent-native browser hooks and utilities -- [**Server**](/docs/server) — file-based server routes and the `/_agent-native/` namespace +- [**Server**](/docs/server-overview) — file-based server routes and the `/_agent-native/` namespace - [**Context Awareness**](/docs/context-awareness) — how the agent learns the current route and selection diff --git a/packages/core/docs/content/server-database.mdx b/packages/core/docs/content/server-database.mdx index b541cdb36a..b5ed0eddbb 100644 --- a/packages/core/docs/content/server-database.mdx +++ b/packages/core/docs/content/server-database.mdx @@ -1,7 +1,6 @@ --- title: "Database" description: "Connect a portable SQL database to your agent-native app: schema helpers, migrations, and the hosted database options." -draft: true --- # Database diff --git a/packages/core/docs/content/server-middleware.mdx b/packages/core/docs/content/server-middleware.mdx index adc391eca7..44fa719b25 100644 --- a/packages/core/docs/content/server-middleware.mdx +++ b/packages/core/docs/content/server-middleware.mdx @@ -1,7 +1,6 @@ --- title: "Middleware" description: "Nitro middleware in server/middleware/ — what it is, how to write your own, and how the built-in auth guard works." -draft: true --- # Middleware diff --git a/packages/core/docs/content/server-overview.mdx b/packages/core/docs/content/server-overview.mdx index c309985f85..3ed0ebc74f 100644 --- a/packages/core/docs/content/server-overview.mdx +++ b/packages/core/docs/content/server-overview.mdx @@ -1,7 +1,6 @@ --- title: "Server" description: "The Nitro server layer in an agent-native app: what it's for, when to reach for custom routes, and how its pieces connect." -draft: true --- # Server diff --git a/packages/core/docs/content/server-plugins.mdx b/packages/core/docs/content/server-plugins.mdx index 089998ce02..45d438f324 100644 --- a/packages/core/docs/content/server-plugins.mdx +++ b/packages/core/docs/content/server-plugins.mdx @@ -1,7 +1,6 @@ --- title: "Plugins" description: "Startup plugins in server/plugins/ — what they are, the two plugins every app ships, the optional database plugin, and how to write your own." -draft: true --- # Plugins diff --git a/packages/core/docs/content/server-routes.mdx b/packages/core/docs/content/server-routes.mdx index fe05dea1ae..4c928d974f 100644 --- a/packages/core/docs/content/server-routes.mdx +++ b/packages/core/docs/content/server-routes.mdx @@ -1,7 +1,6 @@ --- title: "Routes" description: "File-based Nitro server routes, the SSR catch-all, naming conventions, and scoping custom routes to the authenticated user." -draft: true --- # Routes diff --git a/packages/core/docs/content/server.mdx b/packages/core/docs/content/server.mdx deleted file mode 100644 index 17a1b47855..0000000000 --- a/packages/core/docs/content/server.mdx +++ /dev/null @@ -1,469 +0,0 @@ ---- -title: "Server" -description: "Nitro server routes, plugins, framework-mounted routes, request context, and SQL-backed sync." ---- - -# Server - -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. - - - -```html -
-
-
Browser / UI
-
Agent loop
-
- External clients
HTTP · MCP · A2A -
-
- -
- Nitro server -
- Actionsdefault surface -
-
- /_agent-native/*framework routes -
-
- /api/*custom file routes -
-
- pluginsstartup: migrations, jobs -
-
- -
- SQL database
Drizzle · the coordination point -
-
-``` - -```css -.diagram-server { - display: flex; - align-items: center; - gap: 14px; - flex-wrap: wrap; -} -.diagram-server .diagram-col { - display: flex; - flex-direction: column; - gap: 10px; -} -.diagram-server .diagram-panel { - display: flex; - flex-direction: column; - gap: 8px; - padding: 14px 16px; -} -.diagram-server .diagram-row { - display: flex; - align-items: center; - gap: 8px; -} -.diagram-server .diagram-arrow { - font-size: 22px; - line-height: 1; -} -``` - -
- -## File-Based Routes {#file-based-routes} - -Routes live in `server/routes/` and Nitro maps filenames to methods and paths: - -```text -server/routes/ - api/ - health.get.ts -> GET /api/health - uploads.post.ts -> POST /api/uploads - webhooks/ - stripe.post.ts -> POST /api/webhooks/stripe - [...page].get.ts -> SSR catch-all for public pages -``` - -Each route exports a `defineEventHandler`: - -```ts filename="server/routes/api/health.get.ts" -import { defineEventHandler } from "h3"; - -export default defineEventHandler(() => ({ - ok: true, - service: "my-template", -})); -``` - -### Route naming conventions {#route-naming-conventions} - -| File name pattern | HTTP method | Example path | -| ------------------ | ----------- | --------------------------- | -| `index.get.ts` | GET | `/api/items` | -| `index.post.ts` | POST | `/api/items` | -| `[id].get.ts` | GET | `/api/items/:id` | -| `[id].patch.ts` | PATCH | `/api/items/:id` | -| `[id].delete.ts` | DELETE | `/api/items/:id` | -| `[...slug].get.ts` | GET | `/api/items/*` or catch-all | - -## Prefer Actions For App Operations {#actions-first} - -If the UI and agent both need to do something, define an action instead of a custom API route. Actions automatically become: - -- Agent tools. -- Typed frontend hooks. -- HTTP endpoints under `/_agent-native/actions/:name`. -- MCP and A2A-callable tools. -- CLI commands for development. - -Use custom `/api/*` routes only when you need a route-shaped protocol or binary/streaming behavior. See [Actions](/docs/actions). - -### API-Only Apps {#api-only-apps} - -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/` with schema validation and request context. - -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. - -For a read-only status endpoint, keep it as an action: - -```ts filename="actions/get-status.ts" -import { defineAction } from "@agent-native/core/action"; -import { z } from "zod"; - -export default defineAction({ - description: "Return API service status", - schema: z.object({ - service: z.string().optional(), - }), - http: { method: "GET" }, - run: async ({ service }) => ({ - ok: true, - service: service ?? "api", - checkedAt: new Date().toISOString(), - }), -}); -``` - -Call it directly over HTTP: - -```http -GET /_agent-native/actions/get-status?service=billing -``` - -For `POST` and other write actions, send a JSON body to the same action path. - -## One-Shot Text Completion {#complete-text} - -Most AI work should go through the agent chat so users can see, steer, and audit -what happened. For narrow server-side transforms that intentionally do not need -tools, chat history, or run state, use `completeText()` as an explicit escape -hatch. - -```ts filename="actions/classify-message.ts" -import { defineAction } from "@agent-native/core/action"; -import { completeText } from "@agent-native/core/server"; -import { z } from "zod"; - -export default defineAction({ - description: "Classify a short message", - schema: z.object({ body: z.string() }), - run: async ({ body }) => { - const result = await completeText({ - systemPrompt: - "Return exactly one label: urgent, follow-up, waiting, or archive.", - input: body, - maxOutputTokens: 16, - temperature: 0, - }); - - return { label: result.text.trim() }; - }, -}); -``` - -`completeText()` runs through the same configured engine layer as the agent -chat, including Builder, Anthropic, AI SDK providers, user/app model defaults, -request-scoped secrets, and engine-normalized errors. It is server-only; do not -call model providers from client code. If the operation is user-facing, wrap it -in an action so the UI and agent share the same capability. - -## Request Context And Access {#request-context} - -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: - - {\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});' - } - annotations={[ - { - lines: "7-11", - label: "Custom routes have no auto-context", - note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.", - }, - { - lines: "12-13", - label: "Establish request context", - note: "`runWithRequestContext` makes the user/org available to scoping helpers for the duration of the work.", - }, - { - lines: "18-19", - label: "Scope ownable reads", - note: "`accessFilter` constrains the query to rows the caller may see. Never run an unscoped `db.select().from(ownableTable)` here.", - }, - ]} -/> - -`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. - -## Server Plugins {#server-plugins} - -Plugins live in `server/plugins/` and run at startup. Use them for migrations, provider setup, recurring jobs, integration adapters, and framework plugin configuration. - -```ts filename="server/plugins/db.ts" -import { runMigrations } from "@agent-native/core/db"; - -export default runMigrations( - [ - { - version: 1, - sql: `CREATE TABLE IF NOT EXISTS projects ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - owner_email TEXT NOT NULL, - org_id TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - )`, - }, - ], - { table: "my_app_migrations" }, -); -``` - -Migrations must be additive. Never put destructive SQL in startup plugins. - -## Framework-Mounted Routes {#framework-routes} - -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. - -| Route prefix | Purpose | -| -------------------------------- | ------------------------------------------------------------------------------- | -| `/_agent-native/actions/:name` | Action HTTP endpoints | -| `/_agent-native/agent-chat` | Agent chat loop | -| `/_agent-native/poll` | SQL-backed UI sync | -| `/_agent-native/runs` | Progress primitive — the `RunsTray` polls this; see [Progress](/docs/progress) | -| `/_agent-native/resources/*` | Workspace resources | -| `/_agent-native/extensions/*` | Runtime extensions and extension proxy (legacy alias: `/_agent-native/tools/*`) | -| `/_agent-native/integrations/*` | Messaging/webhook integrations | -| `/_agent-native/a2a` | Agent-to-agent JSON-RPC | -| `/mcp` | MCP endpoint | -| `/_agent-native/onboarding/*` | Setup checklist | -| `/_agent-native/observability/*` | Traces, feedback, evals, experiments | -| `/_agent-native/file-upload` | File upload provider endpoint | - -Custom app routes should use `/api/*`, public app paths, or provider-specific callback paths that do not collide with `/_agent-native/`. - -## SQL-Backed Sync {#sync} - -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. - -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. - - - -```html -
-
- Action / helper
mutates data -
- -
- SQL databasesync version increments -
- -
-
- useDbSync()
polls /_agent-native/poll -
-
invalidate caches → UI refreshes
-
-
-``` - -```css -.diagram-sync { - display: flex; - align-items: center; - gap: 14px; - flex-wrap: wrap; -} -.diagram-sync .diagram-col { - display: flex; - flex-direction: column; - gap: 8px; - align-items: flex-start; -} -.diagram-sync .diagram-arrow { - font-size: 22px; - line-height: 1; -} -``` - -
- - - -`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. - - - -## Webhooks {#webhooks} - -Inbound webhooks should verify, persist, and return quickly. Long-running agent work should use the integration queue pattern: - -1. Verify the platform signature or challenge. -2. Insert durable work into SQL. -3. Self-fire a signed processor route. -4. Return 200 immediately. -5. Let the fresh processor execution run the agent loop and post the result. - - - -```html -
-
- Inbound webhook
Slack · Stripe · email -
- -
- Handler -
- 1verify signature -
-
- 2insert work into SQL -
-
- 3self-fire processor -
-
- 4return 200 now -
-
- -
- Signed processor
runs agent loop, posts result -
-
-``` - -```css -.diagram-webhook { - display: flex; - align-items: center; - gap: 14px; - flex-wrap: wrap; -} -.diagram-webhook .diagram-panel { - display: flex; - flex-direction: column; - gap: 6px; - padding: 14px 16px; -} -.diagram-webhook .diagram-step { - display: flex; - align-items: center; - gap: 8px; -} -.diagram-webhook .diagram-arrow { - font-size: 22px; - line-height: 1; -} -``` - -
- - - Do not rely on unawaited promises after returning a response — serverless - hosts freeze the execution. See [Messaging](/docs/messaging) for the canonical - integration queue. - - -## Advanced: Escape Hatches {#advanced-escape-hatches} - -Most templates never need these. Nitro file routes and the framework's agent -chat plugin already wire up the app server and the production agent handler. -Reach for them only when building a custom server integration outside the -standard template plugin stack. - -### Programmatic H3 servers {#create-server} - -For custom packages or tests that need an H3 app directly, `createServer()` -returns a preconfigured app and router: - -```ts -import { createServer } from "@agent-native/core/server"; -import { defineEventHandler } from "h3"; - -const { app, router } = createServer(); - -router.get( - "/api/health", - defineEventHandler(() => ({ ok: true })), -); -``` - -### Production agent handler {#agent-handler} - -The framework's agent chat plugin already mounts the production agent handler -for templates. Only call `createProductionAgentHandler()` directly when building -a custom server integration outside the standard template plugin stack — -otherwise customize the agent through `AGENTS.md`, skills, actions, and the -agent chat plugin. - -```ts -import { createProductionAgentHandler } from "@agent-native/core/server"; - -const handler = createProductionAgentHandler({ - scripts, - systemPrompt: "You are the app agent...", -}); -``` - -## What's next - -- [**Actions**](/docs/actions) — the default operation surface; reach for a custom route only when it doesn't fit -- [**Database**](/docs/database) — the `getDb()`/`schema` pattern custom routes and actions both import -- [**Security**](/docs/security#access-guards) — `accessFilter`/`assertAccess` and the full data-scoping model -- [**Messaging**](/docs/messaging) — the canonical inbound-webhook integration queue pattern -- [**MCP Protocol**](/docs/mcp-protocol) — the `/mcp` endpoint this server mounts automatically diff --git a/packages/docs/app/components/docsNavItems.ts b/packages/docs/app/components/docsNavItems.ts index 19c3005f54..3d056bc9fb 100644 --- a/packages/docs/app/components/docsNavItems.ts +++ b/packages/docs/app/components/docsNavItems.ts @@ -85,41 +85,34 @@ const NAV_SECTION_CONFIG: NavSectionConfig[] = [ id: "core-architecture", titleKey: "coreArchitecture", items: [ - { id: "server", labelKey: "server", slug: "server" }, { id: "server-section", labelKey: "server", - draft: true, children: [ { id: "server-overview", labelKey: "serverOverview", slug: "server-overview", - draft: true, }, { id: "server-database", labelKey: "database", slug: "server-database", - draft: true, }, { id: "server-middleware", labelKey: "serverMiddleware", slug: "server-middleware", - draft: true, }, { id: "server-plugins", labelKey: "serverPlugins", slug: "server-plugins", - draft: true, }, { id: "server-routes", labelKey: "serverRoutes", slug: "server-routes", - draft: true, }, ], },