---
title: "Deployment"
description: "Deploy agent-native apps to any platform with Nitro presets — Node.js, Vercel, Netlify, Cloudflare, AWS, and more."
---

# Deployment

Agent-native apps use [Nitro](https://nitro.build) under the hood, which means you can deploy to any platform with zero config changes — just set a preset.

## Before You Deploy: Pick a Persistent Database {#persistent-database}

Every deployed app needs a persistent SQL database. In local development, agent-native falls back to a SQLite file at `data/app.db`; that is convenient on your machine, but it is not durable in containers, previews, or serverless environments where the filesystem can be reset.

Set `DATABASE_URL` in your deploy provider before promoting an app to production. Agent-native uses Drizzle for schema and queries, so the data layer is portable across Drizzle-compatible SQL backends and the framework auto-detects the dialect from the URL. See [Database](/docs/database#production) for the adapter list and dialect details.

Use `DATABASE_AUTH_TOKEN` only when your database provider requires a separate token, such as Turso/libSQL. For workspaces, all apps inherit the root `DATABASE_URL` by default; set `<APP_NAME>_DATABASE_URL` when one app should use a different database.

## Workspace Deploy: One Origin, Many Apps {#workspace-deploy}

If your project is a [workspace](/docs/multi-app-workspace), you can ship every app in it to a single origin with one command:

```bash
npx @agent-native/core@latest deploy
# https://your-agents.com/mail/*       → apps/mail
# https://your-agents.com/calendar/*   → apps/calendar
# https://your-agents.com/forms/*      → apps/forms
```

Each app is built with `APP_BASE_PATH=/<name>` and `VITE_APP_BASE_PATH=/<name>`, then packaged for the target Nitro preset. Cloudflare Pages is the default preset and uses a generated dispatcher worker at `dist/_worker.js`; Netlify uses one function per app in `.netlify/functions-internal/<app>-server` plus generated redirects; Vercel writes a workspace-level `.vercel/output` using the Build Output API.

<Diagram id="doc-block-1aw1umr" title="One origin, many apps" summary={"Each workspace app is built with its own base path and mounted under a path prefix on a single origin — so login and cross-app A2A are same-origin and free."}>

```html
<div class="diagram-ws">
  <div class="diagram-panel" data-rough>
    <strong>https://your-agents.com</strong>
    <div class="diagram-row">
      <span class="diagram-pill accent">/mail/*</span
      ><small class="diagram-muted">apps/mail</small>
    </div>
    <div class="diagram-row">
      <span class="diagram-pill accent">/calendar/*</span
      ><small class="diagram-muted">apps/calendar</small>
    </div>
    <div class="diagram-row">
      <span class="diagram-pill accent">/forms/*</span
      ><small class="diagram-muted">apps/forms</small>
    </div>
  </div>
  <div class="diagram-col wins">
    <span class="diagram-pill ok">shared login session</span
    ><span class="diagram-pill ok">zero-config cross-app A2A</span>
  </div>
</div>
```

```css
.diagram-ws {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.diagram-ws .diagram-panel {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 16px;
}
.diagram-ws .diagram-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.diagram-ws .wins {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-start;
}
```

</Diagram>

Same-origin deploy gives you two big wins for free:

- **Shared login session** — log into any app, every app is logged in.
- **Zero-config cross-app A2A** — tagging `@calendar` from mail is a same-origin fetch; no CORS, no JWT signing between siblings.

Publish the output with:

```bash
wrangler pages deploy dist
```

For Netlify unified deploys, use the Netlify preset:

```bash
npx @agent-native/core@latest deploy --preset netlify
```

For Vercel unified deploys, use the Vercel preset:

```bash
npx @agent-native/core@latest deploy --preset vercel
```

When configuring a provider build command, use the same command with `--build-only`. Vercel should run `npx @agent-native/core@latest deploy --preset vercel --build-only`; the command writes `.vercel/output` directly, so no `vercel.json` is required for workspace routing.

Hosted workspace builds require `A2A_SECRET` in the deploy provider environment.
This makes Slack, inbound webhooks, and cross-app A2A resume work through signed
background processors. Local `--build-only` artifact checks still run without it.

Per-app independent deploy is still supported — just `cd apps/<name> && npx @agent-native/core@latest build` like a standalone scaffold.

## How It Works {#how-it-works}

When you run `npx @agent-native/core@latest build`, Nitro builds both the client SPA and the server API into `.output/`:

<FileTree
  id="doc-block-1vbo81"
  title="Build output"
  entries={[
    {
      path: ".output/",
      note: "self-contained — copy to any environment and run",
    },
    {
      path: ".output/public/",
      note: "built SPA (static assets)",
    },
    {
      path: ".output/server/index.mjs",
      note: "server entry point",
    },
    {
      path: ".output/server/chunks/",
      note: "server code chunks",
    },
  ]}
/>

The output is self-contained — copy `.output/` to any environment and run it.

<Diagram id="doc-block-1gqwzzp" title="Build to deploy" summary={"One source tree builds to a Nitro preset; the same self-contained output runs on Node, Vercel, Netlify, Cloudflare, AWS, or Deno. Every instance points at the same persistent DATABASE_URL."}>

```html
<div class="diagram-deploy">
  <div class="diagram-box" data-rough>App source</div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel center" data-rough>
    <span class="diagram-pill accent">build</span
    ><small class="diagram-muted">Nitro preset</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-grid">
    <span class="diagram-pill">Node.js</span
    ><span class="diagram-pill">Vercel</span
    ><span class="diagram-pill">Netlify</span
    ><span class="diagram-pill">Cloudflare</span
    ><span class="diagram-pill">AWS Lambda</span
    ><span class="diagram-pill">Deno</span>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box" data-rough>
    Persistent DATABASE_URL<br /><small class="diagram-muted"
      >shared by every instance</small
    >
  </div>
</div>
```

```css
.diagram-deploy {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.diagram-deploy .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 14px 16px;
}
.diagram-deploy .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
.diagram-deploy .diagram-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
```

</Diagram>

## Setting the Preset {#setting-the-preset}

By default, Nitro builds for Node.js. To target a different platform, set the preset in your `vite.config.ts`:

```ts
import { agentNative } from "@agent-native/core/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "vercel" } })],
});
```

Or use the `NITRO_PRESET` environment variable at build time:

```bash
NITRO_PRESET=netlify npx @agent-native/core@latest build
```

## Node.js (Default) {#nodejs}

The default preset. Build and run:

```bash
npx @agent-native/core@latest build
node .output/server/index.mjs
```

Set `PORT` to configure the listen port (default: `3000`).

Use the current Node.js LTS line for production deploys. As of May 2026, that
is Node.js 24; Node.js 20 reached end-of-life on April 30, 2026 and no longer
receives upstream security updates.

### Docker {#docker}

```dockerfile filename="Dockerfile"
FROM node:24-slim AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:24-slim
WORKDIR /app
COPY --from=build /app/.output .output
# data/ is a runtime-created SQLite directory — do not copy a dev DB into prod.
# For production, set DATABASE_URL to a hosted Postgres or Turso instance.
RUN mkdir -p /app/data
ENV PORT=3000
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
```

### Self-hosted workspace with Docker Compose {#workspace-docker-compose}

The Dockerfile above is for one standalone app. A workspace is not one Nitro
server process today: each app still builds to its own `.output/` directory and
runs its own Node/Nitro server. For a VPS, run one container per workspace app,
point every app at the same Postgres database and shared production secrets, and
put a reverse proxy in front that routes path prefixes (`/mail`, `/calendar`,
...) to the matching app container.

Build each app with the same base path that the proxy will use at runtime. The
base path is a build-time input for the client bundle and a runtime input for
the server:

```bash maxLines=8
APP_BASE_PATH=/mail VITE_APP_BASE_PATH=/mail npx @agent-native/core@latest build
node .output/server/index.mjs
```

A reusable workspace Dockerfile can take the app name and path prefix as build
arguments:

```dockerfile filename="Dockerfile.workspace-app" maxLines=12
FROM node:24-slim AS build
WORKDIR /workspace
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages ./packages
COPY apps ./apps
ARG APP_NAME
ARG APP_BASE_PATH
ENV APP_BASE_PATH=$APP_BASE_PATH
ENV VITE_APP_BASE_PATH=$APP_BASE_PATH
RUN pnpm install --frozen-lockfile
RUN pnpm --dir apps/$APP_NAME build

FROM node:24-slim
WORKDIR /app
ARG APP_NAME
ARG APP_BASE_PATH
ENV NODE_ENV=production
ENV PORT=3000
ENV APP_BASE_PATH=$APP_BASE_PATH
COPY --from=build /workspace/apps/$APP_NAME/.output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
```

Then compose Postgres, the app containers, and your proxy. This example uses
Caddy because it can terminate HTTP locally or sit behind Cloudflare's SSL
proxy, but the important part is the path-prefix routing:

```yaml filename="docker-compose.yml" maxLines=30
services:
  postgres:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: agent_native
      POSTGRES_USER: agent_native
      POSTGRES_PASSWORD: change-me
    volumes:
      - postgres-data:/var/lib/postgresql/data

  mail:
    build:
      context: .
      dockerfile: Dockerfile.workspace-app
      args:
        APP_NAME: mail
        APP_BASE_PATH: /mail
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
      BETTER_AUTH_URL: https://agents.example.com/mail
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
      A2A_SECRET: ${A2A_SECRET}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
    depends_on:
      - postgres

  calendar:
    build:
      context: .
      dockerfile: Dockerfile.workspace-app
      args:
        APP_NAME: calendar
        APP_BASE_PATH: /calendar
    restart: unless-stopped
    environment:
      DATABASE_URL: postgres://agent_native:change-me@postgres:5432/agent_native
      BETTER_AUTH_URL: https://agents.example.com/calendar
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
      A2A_SECRET: ${A2A_SECRET}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
    depends_on:
      - postgres

  proxy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    depends_on:
      - mail
      - calendar

volumes:
  postgres-data:
```

```caddyfile filename="Caddyfile"
agents.example.com {
  reverse_proxy /mail* mail:3000
  reverse_proxy /calendar* calendar:3000
}
```

Notes for this shape:

- Keep `DATABASE_URL`, `BETTER_AUTH_SECRET`, `A2A_SECRET`, and other production
  secrets identical across app containers when you want shared auth, shared
  credentials, and cross-app A2A.
- Set `BETTER_AUTH_URL` to the public URL for that app path. OAuth callback URLs
  should use the same public URL.
- Do not rely on the container filesystem for production data; use Postgres (or
  another persistent SQL backend) for `DATABASE_URL`.
- If Cloudflare terminates TLS in front of the VPS, configure Cloudflare to send
  normal `X-Forwarded-Proto` / `X-Forwarded-Host` headers and route the public
  host to the proxy container.

## Vercel {#vercel}

```ts filename="vite.config.ts"
export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "vercel" } })],
});
```

Deploy via the Vercel CLI or git push:

```bash
vercel deploy
```

For a workspace, build every app into one Vercel Build Output API bundle:

```bash
npx @agent-native/core@latest deploy --preset vercel
```

For Vercel Git deployments, set the build command to:

```bash
npx @agent-native/core@latest deploy --preset vercel --build-only
```

The workspace build copies each app's Nitro `vercel` output into the root `.vercel/output`, gives each function its own mount-path environment, and writes the route config that serves apps at `/<app-id>`.

## Netlify {#netlify}

The Nitro `netlify` preset works well and, in practice, has given us much faster cold starts than Cloudflare Pages (~200ms TTFB vs ~9s) for templates that talk to external Postgres (Neon). Either set the preset in `vite.config.ts`:

```ts filename="vite.config.ts"
export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "netlify" } })],
});
```

…or set `NITRO_PRESET=netlify` at build time.

For a workspace, deploy every app from one Netlify site by running:

```bash
npx @agent-native/core@latest deploy --preset netlify
```

The workspace build writes static assets under `dist/_workspace_static/` and routes each app to its own Netlify function without forced asset redirects, so files like `/mail/assets/...` are served statically before the server function handles app routes.

## Cloudflare Pages {#cloudflare-pages}

```ts filename="vite.config.ts"
export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "cloudflare_pages" } })],
});
```

## Cloudflare Workers {#cloudflare-workers}

Use Nitro's native module output for a Cloudflare Worker. This keeps SSR
enabled and emits the Worker entry point plus static assets under
`.output/server` and `.output/public`:

```ts filename="vite.config.ts"
export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "cloudflare_module" } })],
});
```

Build and deploy the generated Worker with Wrangler:

```bash
npx @agent-native/core@latest build
npx wrangler deploy --config .output/server/wrangler.json
```

You can also select the preset explicitly with
`NITRO_PRESET=cloudflare_module`. Configure a durable external SQL database
before deploying; a Worker filesystem is not a persistent application store.

## AWS Lambda {#aws-lambda}

```ts filename="vite.config.ts"
export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "aws_lambda" } })],
});
```

## Deno Deploy {#deno-deploy}

```ts filename="vite.config.ts"
export default defineConfig({
  plugins: [agentNative({ nitro: { preset: "deno_deploy" } })],
});
```

## SSR Caching {#ssr-caching}

Every SSR HTML response and every React Router `.data` response is one impersonal, public shell: `createH3SSRHandler` strips cookies before rendering, so the same bytes are correct for every visitor and all personalization happens on the client after load. That is what makes it safe to stamp a single hard-cache policy on those responses by default:

```txt
cache-control: public, max-age=600, stale-while-revalidate=604800, stale-if-error=3600
```

The same value is mirrored onto `cdn-cache-control` and `netlify-cdn-cache-control`. One shared CDN entry then serves the whole site instead of a per-request render, which is the single biggest lever on first-response latency — leave it on unless one of the cases below applies to your deployment.

### Overriding with `AGENT_NATIVE_SSR_CACHE` {#ssr-cache-env}

Set `AGENT_NATIVE_SSR_CACHE` in the deploy environment to change the policy for the whole deployment:

| Value                                               | Result                                                                               |
| --------------------------------------------------- | ------------------------------------------------------------------------------------ |
| unset, `on`, `default`, `true`, `1`                 | The default policy above, unchanged. Recommended — best performance.                 |
| `off`, `false`, `0`, `none`, `no-store`, `disabled` | `no-store` on `cache-control`, `cdn-cache-control`, and `netlify-cdn-cache-control`. |
| A duration: `30`, `30s`, `5m`, `2h`                 | `public, max-age=<n>, stale-while-revalidate=<n>, stale-if-error=3600`.              |

Bare numbers are seconds. `stale-while-revalidate` deliberately mirrors `max-age` for a custom duration: a short freshness window paired with the default seven-day stale window would hand back exactly the staleness you opted out of. An unrecognized value logs a warning and falls back to the default, so a typo can never silently disable the CDN.

Turn it down or off when either of these is true:

1. **Your host does not purge its CDN on deploy.** Netlify and Vercel do; some self-managed setups do not. Without a purge, a shipped build can keep serving the previous shell for `max-age` plus `stale-while-revalidate`.
2. **Your loaders return mutable public data.** After a successful mutation, a `useRevalidator()` or redirect-after-action re-fetch can read the browser's cached `.data` copy instead of fresh loader output.

The setting applies to every public-shell surface: React Router SSR HTML and `.data`, the login HTML shell, the `/_agent-native/speculation-rules.json` route, the docs site, and the public-form SSR in the forms template. The generated Cloudflare Worker resolves it at build time, so set it in the deploy environment before the build runs.

Three things to keep in mind:

- **Turning caching off does not make SSR personalized.** Cookies are still stripped before render and SSR loaders still see the anonymous branch. Per-user data must still be resolved client-side. This variable controls cache _duration_ only.
- **It is deployment-wide, not per-route, by design.** A per-route or per-request cache override is how one visitor's payload ends up in another visitor's shared CDN entry, so the framework does not offer one — `guard:ssr-cache-shell` enforces it.
- **For mutation-fresh data, prefer actions over disabling the cache.** App data belongs in [actions](/docs/actions), read from the client with `useActionQuery` / `useActionMutation` and kept live by `useDbSync()` polling; none of that goes through the SSR shell cache. Keep SSR loaders rendering the public shell, and reach for this variable only when you genuinely want loader data fresher.

## Environment Variables {#environment-variables}

### Build / Runtime {#env-runtime}

| Variable                                    | Description                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT`                                      | Server port (Node.js only)                                                                                                                                                                                                                                                                                                                                              |
| `NITRO_PRESET`                              | Override build preset at build time                                                                                                                                                                                                                                                                                                                                     |
| `APP_BASE_PATH`                             | Mount the app under a prefix (e.g. `/mail`). Set automatically by `npx @agent-native/core@latest deploy`; leave unset for standalone.                                                                                                                                                                                                                                   |
| `APP_URL`                                   | Optional canonical public origin. Checked ahead of `BETTER_AUTH_URL` when resolving the A2A JWT audience, the OAuth public origin, self-dispatch webhook targets, and generated resource links. Set it explicitly on preview/staging deploys where the request host is unreliable.                                                                                      |
| `AGENT_PROD_CODE_EXECUTION`                 | Optional production code-execution mode: `off` (default), `sandboxed`, or `trusted`. See [Production Code Execution](#production-code-execution).                                                                                                                                                                                                                       |
| `AGENT_NATIVE_SSR_CACHE`                    | Deployment-wide SSR shell cache policy: unset/`on` keeps the default hard-cache, `off` sends `no-store`, or a duration such as `30s` / `5m` sets a shorter freshness. See [SSR Caching](#ssr-caching).                                                                                                                                                                  |
| `AGENT_NATIVE_BUILDER_RELAY_SECRET`         | Dedicated 32+ char shared HMAC secret for preview-safe Builder authorization relay. Set the same value on the approved corporate callback deployment and preview deployments only when authorization must land in an isolated preview database.                                                                                                                         |
| `AGENT_NATIVE_BUILDER_RELAY_TARGET_ORIGINS` | Corporate callback-only, comma-separated allowlist of exact trusted preview origins (for example, `https://0123456789abcdef01234567--content.netlify.app`). Set it only on the corporate callback deployment. Netlify origins must use an immutable, deploy-specific 24-hex permalink; mutable `deploy-preview-*` aliases, wildcards, and domain suffixes are rejected. |

Database connection variables (`DATABASE_URL`, `DATABASE_AUTH_TOKEN`, per-app `<APP_NAME>_DATABASE_URL`) live in [Database](/docs/database#production).

### Required in Production {#env-required-prod}

These must be set before promoting an app to a real prod deploy. Missing values either fail-closed (the framework refuses to start / refuses to handle requests) or fall back to weaker behavior with a loud warning.

| Variable                 | Description                                                                                                                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BETTER_AUTH_SECRET`     | 32+ char random string. Signs session cookies AND is the fallback HMAC for `OAUTH_STATE_SECRET` and `SECRETS_ENCRYPTION_KEY`. Hard-required: the framework throws on startup if missing in production.                                            |
| `BETTER_AUTH_URL`        | Public origin of this app (e.g. `https://mail.example.com`). Used for cookie domain and OAuth redirect construction.                                                                                                                              |
| `ANTHROPIC_API_KEY`      | API key for the embedded production agent. **In multi-tenant deploys**, the framework refuses to fall back to this when the user has no per-user key — bring-your-own-key is required. Single-tenant self-hosted installs use it as a global key. |
| `OAUTH_STATE_SECRET`     | Dedicated HMAC key for OAuth state envelopes (Google, Atlassian, Zoom). Falls back to `BETTER_AUTH_SECRET` when unset, but a dedicated value is recommended so rotating one doesn't invalidate the other. Generate via `openssl rand -hex 32`.    |
| `A2A_SECRET`             | Shared HMAC for inter-app A2A JSON-RPC. Without it, every A2A endpoint and the `/_agent-native/integrations/process-task` self-fire endpoint return 503 in production.                                                                            |
| `SECRETS_ENCRYPTION_KEY` | AES-256-GCM key for the encrypted-at-rest secrets vault. Falls back to `BETTER_AUTH_SECRET`. Hard-fails in production when both are unset.                                                                                                        |

### Auth & Identity {#env-auth}

OAuth provider credentials (Google, GitHub), static MCP bearer fallbacks (`ACCESS_TOKEN` / `ACCESS_TOKENS`), and email-verification toggles are documented in [Authentication](/docs/authentication). Set them there per the auth mode you choose.

### Inbound Webhooks {#env-webhooks}

Each messaging integration requires its own signing secret in production (handlers fail-closed on forged requests when the secret is missing). The per-integration variables are listed in [Messaging](/docs/messaging) and [Security](/docs/security). For local development only, `AGENT_NATIVE_ALLOW_UNVERIFIED_WEBHOOKS=1` opts back into "warn and accept" — never set it in prod.

### Security Configuration (Opt-in) {#security-config}

Defaults are strict. A handful of opt-in flags relax behavior (debug stack traces, unverified webhooks, workspace-scoped key fallback, the MCP hub multi-org switch, runtime env-var writes). They are documented with their security trade-offs in [Security](/docs/security). Don't set them unless you specifically want the relaxed path.

### Workspace .env Inheritance {#env-inheritance}

Inside a workspace, the root `.env` is loaded into every app automatically, so shared keys like `ANTHROPIC_API_KEY`, `A2A_SECRET`, `BETTER_AUTH_SECRET`, and `OAUTH_STATE_SECRET` only need to be set once. Per-app `apps/<name>/.env` wins on conflict.

### Generating Strong Secrets {#env-generate-secrets}

For any secret marked "32+ char random" (`BETTER_AUTH_SECRET`, `OAUTH_STATE_SECRET`, `A2A_SECRET`, `SECRETS_ENCRYPTION_KEY`, `AGENT_NATIVE_BUILDER_RELAY_SECRET`), generate fresh values with:

```bash
openssl rand -hex 32
```

Rotate them by replacing the env var on every instance and redeploying — sessions / OAuth state envelopes signed under the old key become invalid, so users may need to sign in again.

## Production Agent Tools {#production-agent-tools}

Production agents get the app's registered actions plus framework tools from
the agent chat plugin. SQL inspection is enabled by default, while database
writes go through typed app actions unless the app explicitly opts into raw
write tools:

```ts filename="server/plugins/agent-chat.ts"
export default createAgentChatPlugin({
  // Default: "read"
  databaseTools: "read", // "write" | "read" | "off"
  extensionTools: false,
});
```

- `databaseTools: "read"` — default. Registers only `db-schema` and `db-query`;
  agents inspect data with SQL but must use typed app actions for writes.
- `databaseTools: "write"` or `true` — additionally registers `db-exec` and
  `db-patch` for deliberate raw SQL maintenance. Writes are scoped to the
  current user/org and schema changes are blocked.
- `databaseTools: "off"` or `false` — removes raw database tools from the
  agent surface so the app's actions are the only data access path.
- `extensionTools: false` — removes framework extension-management actions and
  prompt guidance (`create-extension`, `update-extension`, etc.) for apps that
  do not want the agent creating sandboxed mini-apps.

## Production Code Execution {#production-code-execution}

By default, production agents run without code-execution tools. They can call app actions, database tools, MCP tools, browser/session tools, and other registered framework tools, but they do not get shell or filesystem access.

Node-compatible deployments can opt into production code execution through the agent chat plugin or an environment override:

```ts filename="server/plugins/agent-chat.ts"
export default createAgentChatPlugin({
  codeExecution: { production: "sandboxed" },
});
```

The available modes are:

- `off` — the default. No code-execution tools are registered in production.
- `sandboxed` — registers `run-code`, an isolated Node.js JavaScript runner with a scrubbed environment, a fresh temp directory, output/time limits, and a localhost bridge to allowlisted registered tools such as `provider-api-request`, `provider-api-docs`, `provider-api-catalog`, `web-request`, and the Resources-backed workspace file bridge used by `workspaceRead` / `workspaceWrite`.
- `trusted` — registers `run-code` plus the full coding tool registry (`bash`, `read`, `edit`, `write`). Use this only for single-tenant or operator-controlled deployments where full shell access to the host is intentional.

Set `AGENT_PROD_CODE_EXECUTION=sandboxed` or `AGENT_PROD_CODE_EXECUTION=trusted` to override the plugin option for a specific deployment without a code change. `AGENT_PROD_CODE_EXECUTION=off` forces code execution off even when the plugin option enables it.

The `run-code` sandbox is process-level isolation, not an OS container. It strips app secrets from the child process environment and uses the Node permission model when available, but outbound network is not blocked by Node itself; authenticated calls should go through the bridge helpers the tool exposes.

## Updating UI in Production {#updating-ui-in-production}

One of agent-native's core features is that the agent can modify your app's source code — components, routes, styles, actions. During local development this works seamlessly because the agent has full filesystem access.

In a standard production deployment with [production code execution](#production-code-execution) left off, the agent has access to app tools (actions, database, MCP) but not the filesystem. This means the agent can read and write data, run actions, and interact with external services — but it can't edit your React components or add new routes on a deployed instance.

### Builder.io: Visual Editing in Production {#builderio}

[Builder.io](https://www.builder.io?utm_source=agent-native&utm_medium=docs&utm_campaign=framework&utm_content=deployment) solves this by providing a managed cloud environment where the agent retains the ability to modify your app's UI in production. Connect your repo to Builder.io and prompt for UI changes directly — no redeploy needed.

**How it works:**

1. Connect your agent-native repo to Builder.io
2. Builder.io provides a cloud frame with the agent, visual editing, and real-time collaboration
3. Prompt the agent to make UI changes — it edits your components, routes, and styles live
4. Changes are committed back to your repo

See [Frames](/docs/frames) for more on the embedded agent panel vs. cloud frame options.

## Multi-instance deploys {#multi-instance}

Agent-native apps store all state in SQL via Drizzle and sync the UI via [polling](/docs/key-concepts#polling-sync) against the database — no file-system state, no sticky sessions, no in-memory caches. That means multi-instance and serverless deployments work out of the box: point every instance at the same `DATABASE_URL` and they converge automatically. See [Key Concepts — Data in SQL](/docs/key-concepts#data-in-sql) and [Portability](/docs/key-concepts#hosting-agnostic).

## What's next

- [**Database**](/docs/database#production) — pick a persistent SQL backend before your first production deploy
- [**Security**](/docs/security) — the production checklist and opt-in flags referenced throughout this page
- [**Authentication**](/docs/authentication) — OAuth credentials and env vars for the auth mode you choose
- [**Multi-App Workspace**](/docs/multi-app-workspace) — the workspace model behind `deploy`'s one-origin build
- [**Frames**](/docs/frames) — the embedded agent panel vs. Builder.io's cloud frame for production UI edits
- [**Public Agent Web**](/docs/agent-web-surfaces) — the `robots.txt`, `llms.txt`, and markdown-mirror files a Vite build writes into the deploy output for public routes
