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/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/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
new file mode 100644
index 0000000000..b5ed0eddbb
--- /dev/null
+++ b/packages/core/docs/content/server-database.mdx
@@ -0,0 +1,423 @@
+---
+title: "Database"
+description: "Connect a portable SQL database to your agent-native app: schema helpers, migrations, and the hosted database options."
+---
+
+# Database
+
+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
+
+```
+
+```css
+.diagram-dbr {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ flex-wrap: wrap;
+}
+.diagram-dbr .diagram-col {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.diagram-dbr .diagram-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 12px 16px;
+}
+.diagram-dbr .diagram-arrow {
+ font-size: 22px;
+ line-height: 1;
+}
+```
+
+
+
+## Hosting Options {#hosting}
+
+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.
+
+### Default: SQLite file {#default-sqlite}
+
+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.
+
+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
+```
+
+```bash
+DATABASE_URL=pglite:./data/pglite
+```
+
+[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.
+
+### Production database {#production}
+
+Set `DATABASE_URL` in your `.env` file or deploy-provider environment to connect a hosted database:
+
+```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
+
+# Turso (libSQL). Also requires DATABASE_AUTH_TOKEN.
+DATABASE_URL=libsql://my-db-org.turso.io
+```
+
+The framework auto-detects the dialect from the URL prefix and configures Drizzle accordingly.
+
+### 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.
+
+## Setting Up the Database {#setup}
+
+An app that uses the database needs three files:
+
+- `server/db/schema.ts`: table definitions
+- `server/db/index.ts`: the typed DB client singleton
+- `server/plugins/db.ts`: migrations that run at startup
+
+### 1. Define your schema {#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.
+
+```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),
+ 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; 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())` |
+
+
+
+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.
+
+### 2. Create the DB client {#db-client}
+
+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 * as schema from "../server/db/schema.js";
+
+const db = getDb();
+
+const openTasks = await db
+ .select()
+ .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.
+
+**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))
+```
+
+**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`:
+
+```ts filename="server/db/schema.ts"
+import {
+ table,
+ text,
+ ownableColumns,
+ createSharesTable,
+} from "@agent-native/core/db/schema";
+
+export const decks = table("decks", {
+ id: text("id").primaryKey(),
+ title: text("title").notNull(),
+ ...ownableColumns(),
+});
+export const deckShares = createSharesTable("deck_shares");
+```
+
+Then use `accessFilter` from `@agent-native/core/sharing` in list queries instead of a manual `eq` check:
+
+```ts
+import { accessFilter } from "@agent-native/core/sharing";
+
+const rows = await db
+ .select()
+ .from(schema.decks)
+ .where(accessFilter(schema.decks, schema.deckShares));
+```
+
+`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.
+
+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`:
+
+```ts
+import { getDbExec, isPostgres, intType } from "@agent-native/core/db";
+
+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}
+
+| 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
+- [**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
diff --git a/packages/core/docs/content/server-middleware.mdx b/packages/core/docs/content/server-middleware.mdx
new file mode 100644
index 0000000000..44fa719b25
--- /dev/null
+++ b/packages/core/docs/content/server-middleware.mdx
@@ -0,0 +1,149 @@
+---
+title: "Middleware"
+description: "Nitro middleware in server/middleware/ — what it is, how to write your own, and how the built-in auth guard works."
+---
+
+# Middleware
+
+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 `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.
+
+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}
+
+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
+- [**Nitro middleware docs**](https://nitro.build/guide/routing#middleware): full middleware API and patterns
diff --git a/packages/core/docs/content/server-overview.mdx b/packages/core/docs/content/server-overview.mdx
new file mode 100644
index 0000000000..3ed0ebc74f
--- /dev/null
+++ b/packages/core/docs/content/server-overview.mdx
@@ -0,0 +1,234 @@
+---
+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."
+---
+
+# 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
+
+ Middleware→Routes
+
+
+
→
+
+ 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..45d438f324
--- /dev/null
+++ b/packages/core/docs/content/server-plugins.mdx
@@ -0,0 +1,152 @@
+---
+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."
+---
+
+# Plugins
+
+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/
+ auth.ts -> configure auth provider and public paths
+ agent-chat.ts -> mount the agent loop with your actions and system prompt
+```
+
+Apps that need custom database tables add a third:
+
+```text
+server/plugins/
+ db.ts -> run migrations on startup
+```
+
+## 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 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.
+
+## 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 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/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
diff --git a/packages/core/docs/content/server-routes.mdx b/packages/core/docs/content/server-routes.mdx
new file mode 100644
index 0000000000..4c928d974f
--- /dev/null
+++ b/packages/core/docs/content/server-routes.mdx
@@ -0,0 +1,194 @@
+---
+title: "Routes"
+description: "File-based Nitro server routes, the SSR catch-all, naming conventions, and scoping custom routes to the authenticated user."
+---
+
+# 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
deleted file mode 100644
index 0e978affa1..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-10",
- 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
-