---
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.

<Diagram id="doc-block-1cve8m0" title="What runs on the server" summary="Actions are the default. Custom file routes and framework-mounted routes share the same Nitro app and the same SQL database.">

```html
<div class="diagram-server">
  <div class="diagram-col entry">
    <div class="diagram-node">Browser / UI</div>
    <div class="diagram-node">Agent loop</div>
    <div class="diagram-node">
      External clients<br /><small class="diagram-muted"
        >HTTP · MCP · A2A</small
      >
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel" data-rough>
    <strong>Nitro server</strong>
    <div class="diagram-row">
      <span class="diagram-pill accent">Actions</span
      ><small class="diagram-muted">default surface</small>
    </div>
    <div class="diagram-row">
      <span class="diagram-pill">/_agent-native/*</span
      ><small class="diagram-muted">framework routes</small>
    </div>
    <div class="diagram-row">
      <span class="diagram-pill">/api/*</span
      ><small class="diagram-muted">custom file routes</small>
    </div>
    <div class="diagram-row">
      <span class="diagram-pill">plugins</span
      ><small class="diagram-muted">startup: migrations, jobs</small>
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box" data-rough>
    SQL database<br /><small class="diagram-muted"
      >Drizzle · the coordination point</small
    >
  </div>
</div>
```

```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;
}
```

</Diagram>

## 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/<name>` 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:

<AnnotatedCode
  id="doc-block-z4ll"
  title="Scoping a custom route to the request user"
  filename="server/routes/api/projects.get.ts"
  language="ts"
  code={
    'import { defineEventHandler, createError } from "h3";\nimport { getSession, runWithRequestContext } from "@agent-native/core/server";\nimport { getDb } from "../../db/index.js";\nimport { accessFilter } from "@agent-native/core/sharing";\nimport * as schema from "../../db/schema";\n\nexport default defineEventHandler(async (event) => {\n  const session = await getSession(event);\n  if (!session?.email) {\n    throw createError({ statusCode: 401, statusMessage: "Unauthorized" });\n  }\n\n  return runWithRequestContext(\n    { userEmail: session.email, orgId: session.orgId },\n    async () => {\n      const db = getDb();\n      return db\n        .select()\n        .from(schema.projects)\n        .where(accessFilter(schema.projects, schema.projectShares));\n    },\n  );\n});'
  }
  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.

<Diagram id="doc-block-qhkdv8" title="SQL-backed sync loop" summary={"No watchers, no sticky state. A write bumps a version in SQL; every client polls the version and refetches."}>

```html
<div class="diagram-sync">
  <div class="diagram-box" data-rough>
    Action / helper<br /><small class="diagram-muted">mutates data</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel" data-rough>
    <strong>SQL database</strong
    ><small class="diagram-muted">sync version increments</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&larr;</div>
  <div class="diagram-col">
    <div class="diagram-node">
      useDbSync()<br /><small class="diagram-muted"
        >polls /_agent-native/poll</small
      >
    </div>
    <div class="diagram-pill ok">invalidate caches &rarr; UI refreshes</div>
  </div>
</div>
```

```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;
}
```

</Diagram>

<Endpoint id="doc-block-1s11d2e" title="The poll endpoint" method="GET" path="/_agent-native/poll" summary="Return the current per-source database sync versions so the client can detect changes." auth="Session cookie (request-scoped identity)" responses={[
  {
    "status": "200",
    "description": "Current sync versions keyed by source."
  }
]}>

`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.

</Endpoint>

## 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.

<Diagram id="doc-block-1v3q81q" title="Integration queue pattern" summary={"The webhook handler returns in milliseconds; a separate signed execution runs the slow agent work."}>

```html
<div class="diagram-webhook">
  <div class="diagram-box" data-rough>
    Inbound webhook<br /><small class="diagram-muted"
      >Slack · Stripe · email</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel" data-rough>
    <strong>Handler</strong>
    <div class="diagram-step">
      <span class="diagram-pill">1</span
      ><small class="diagram-muted">verify signature</small>
    </div>
    <div class="diagram-step">
      <span class="diagram-pill">2</span
      ><small class="diagram-muted">insert work into SQL</small>
    </div>
    <div class="diagram-step">
      <span class="diagram-pill">3</span
      ><small class="diagram-muted">self-fire processor</small>
    </div>
    <div class="diagram-step">
      <span class="diagram-pill ok">4</span
      ><small class="diagram-muted">return 200 now</small>
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box" data-rough>
    Signed processor<br /><small class="diagram-muted"
      >runs agent loop, posts result</small
    >
  </div>
</div>
```

```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;
}
```

</Diagram>

<Callout tone="warning">
  Do not rely on unawaited promises after returning a response — serverless
  hosts freeze the execution. See [Messaging](/docs/messaging) for the canonical
  integration queue.
</Callout>

## 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
