Deployment

August 7, 2026 · View on GitHub

Agent-native apps use Nitro 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 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, you can ship every app in it to a single origin with one command:

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

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

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:

wrangler pages deploy dist

For Netlify unified deploys, use the Netlify preset:

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

For Vercel unified deploys, use the Vercel preset:

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

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

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:

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:

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

Node.js (Default) {#nodejs}

The default preset. Build and run:

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}

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:

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:

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:

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:
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}

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

Deploy via the Vercel CLI or git push:

vercel deploy

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

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

For Vercel Git deployments, set the build command to:

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:

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:

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.

Keep-warm and scale-to-zero databases {#netlify-keep-warm}

The Netlify build emits a scheduled function that requests /_agent-native/health every minute. That keeps the server function and its database warm so the next visitor after an idle period doesn't pay a cold start (1–5s on Neon). On provisioned compute this is the right default.

On a metered scale-to-zero database it is the wrong default. A once-a-minute wake means autosuspend never fires, so the database bills and quota-limits as though it were awake continuously with nobody using the app. On a free-tier Neon this exhausts the compute quota, and the database then rejects all reads mid-session — the symptom is a dead app, not a slow one.

Three knobs, all opt-in, defaults unchanged:

VariableEffect
AGENT_NATIVE_DISABLE_KEEP_WARMSkips the scheduled function entirely. The database is free to autosuspend; the next visitor after an idle period pays its cold start.
AGENT_NATIVE_KEEP_WARM_SCHEDULEOverrides the cadence with a 5-field cron expression, e.g. */5 * * * *. An unparseable value fails the build rather than silently reverting to once a minute.
AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUNDDrops only the background-function warm, keeping the server warm. Warming server is one request; warming -background starts a fresh container that re-probes the schema.

If you are on a free or otherwise quota-limited database tier, set AGENT_NATIVE_DISABLE_KEEP_WARM=1 or pick a slower cadence.

On-demand schema DDL {#skip-ensure-tables}

Each store calls ensureTable() on its first database touch, which probes information_schema / pg_indexes and issues CREATE TABLE / ADD COLUMN / CREATE INDEX only for what is genuinely missing. On a long-lived Node server that cost is paid once per boot and is invisible. On serverless, "first touch" is every cold start, across ~57 stores — so a cold request can pay a few hundred serial round trips before it does any real work.

Two things reduce it:

  • The probes are answered from one batched introspection pass per database (two queries), instead of one query per table, column, and index. This is automatic and needs no configuration.
  • AGENT_NATIVE_SKIP_ENSURE_TABLES=1 skips the probe-and-DDL machinery altogether, which is the largest cold-start latency win available.

Set AGENT_NATIVE_SKIP_ENSURE_TABLES=1 when your database schema is already migrated and you do not want application boot creating or altering tables — the normal case for a production deployment with a real migration step.

This flag does not and cannot fail closed. If the schema is in fact missing, the first real query fails loudly rather than being repaired on the fly — which is the point, but it means you own the migration story. Do not set it on a deployment that relies on application boot to provision its tables.

Cloudflare Pages {#cloudflare-pages}

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:

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

Build and deploy the generated Worker with Wrangler:

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}

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

Deno Deploy {#deno-deploy}

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:

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:

ValueResult
unset, on, default, true, 1The default policy above, unchanged. Recommended — best performance.
off, false, 0, none, no-store, disabledno-store on cache-control, cdn-cache-control, and netlify-cdn-cache-control.
A duration: 30, 30s, 5m, 2hpublic, 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, 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}

For the repository-wide reference, including template, provider, local, and CI variables, see Environment Variables. The tables below remain the authoritative deployment guide for production values and hosting behavior.

Build / Runtime {#env-runtime}

VariableDescription
PORTServer port (Node.js only)
NITRO_PRESETOverride build preset at build time
APP_BASE_PATHMount the app under a prefix (e.g. /mail). Set automatically by npx @agent-native/core@latest deploy; leave unset for standalone.
APP_URLOptional 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_EXECUTIONOptional production code-execution mode: off (default), sandboxed, or trusted. See Production Code Execution.
AGENT_NATIVE_SSR_CACHEDeployment-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.
AGENT_NATIVE_DISABLE_KEEP_WARMSkip the Netlify keep-warm scheduled function so a scale-to-zero database can autosuspend. See Keep-warm.
AGENT_NATIVE_KEEP_WARM_SCHEDULEOverride the keep-warm cadence with a 5-field cron expression (default * * * * *). An unparseable value fails the build. See Keep-warm.
AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUNDDrop only the background-function warm and keep the server warm. See Keep-warm.
AGENT_NATIVE_DISABLE_INPROCESS_SWEEPSTurn off the in-process backstop sweep timers (automation redispatch, agent-teams reconciliation, sandbox-execution recovery). On serverless these run per warm container, so their query rate scales with instance count. Set it only where a durable scheduler already drives the same recovery, or queued work will not be retried.
AGENT_NATIVE_SKIP_ENSURE_TABLESSkip on-demand schema DDL entirely. Removes the schema-probe round trips every cold start pays before doing any work. Requires that your schema is already migrated — see On-demand schema DDL.
AGENT_NATIVE_BUILDER_RELAY_SECRETDedicated 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_ORIGINSCorporate 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.

Production-sensitive configuration {#env-required-prod}

These settings cover production-sensitive behavior. A standalone app using the framework auth needs a stable BETTER_AUTH_SECRET and a persistent database; the public auth URL is inferred from the request, known template, or hosting platform when it is unset. Workspace and A2A settings are conditional on those features. Missing values either fail-closed or fall back to weaker behavior with a loud warning.

VariableDescription
BETTER_AUTH_SECRET32+ char random string. Signs session cookies and is the fallback for OAUTH_STATE_SECRET and app-local encrypted values. It is also a legacy read candidate for existing shared vault ciphertext. Hard-required: the framework throws on startup if missing in production.
BETTER_AUTH_URLOptional public origin override for this app (e.g. https://mail.example.com). The framework infers it from request, known-template, and hosting-platform context when unset.
ANTHROPIC_API_KEYAPI 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_SECRETDedicated 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_SECRETShared HMAC for inter-app A2A JSON-RPC and signed background handoffs. Required for hosted workspace deploys and A2A features; without it, those endpoints fail closed in production. When no dedicated workspace vault key is set, shared vault ciphertext uses a purpose-derived key from this value, so rotating it also rotates the vault key.
WORKSPACE_SECRETS_ENCRYPTION_KEYPreferred stable AES-256-GCM key for the shared encrypted-at-rest secrets vault. Set the same value in every app that reads the vault. It is separate from app-local OAuth/credential encryption, so A2A trust can rotate without stranding vault rows. Existing A2A-, Better Auth-, and app-key ciphertext is migrated on successful reads.
WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUSOptional previous workspace vault key used only during a rotation window. Deploy it alongside the new WORKSPACE_SECRETS_ENCRYPTION_KEY, let successful reads re-encrypt rows, then remove it after migration.
SECRETS_ENCRYPTION_KEYLegacy combined key for app-local encryption and the shared vault. It remains supported, but new multi-app deployments should use WORKSPACE_SECRETS_ENCRYPTION_KEY for the shared vault so changing workspace encryption does not change app-local OAuth ciphertext.

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. 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 and 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. 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, WORKSPACE_SECRETS_ENCRYPTION_KEY, SECRETS_ENCRYPTION_KEY, AGENT_NATIVE_BUILDER_RELAY_SECRET), generate fresh values with:

openssl rand -hex 32

Rotate signing/session keys 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. Rotate WORKSPACE_SECRETS_ENCRYPTION_KEY with the ..._PREVIOUS window described above so encrypted vault rows remain readable while they migrate.

Production Agent Tools {#production-agent-tools}

Production agents get the app's registered actions plus the framework's own tools. frameworkTools selects which of those framework tools this app exposes. Every group defaults to today's behavior, so omitting the option changes nothing:

export default createAgentChatPlugin({
  frameworkTools: {
    database: "read", // "write" | "read" | "off" — default "read"
    extensions: true, // default false
    sharing: false, // this app has no share surface
    review: false,
  },
});
export default createCoreRoutesPlugin({
  extensionTools: true,
});

database and extensions accept the values the old top-level flags did:

  • database: "read" — default. Registers only db-schema and db-query; agents inspect data with SQL but must use typed app actions for writes.
  • database: "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.
  • database: "off" or false — removes raw database tools from the agent surface so the app's actions are the only data access path.
  • extensions: true — opts the app into framework extension-management actions and prompt guidance (create-extension, update-extension, etc.). Set extensionTools: true on the core-routes plugin as well to allow authenticated REST creation. Extensions are disabled by default.

The remaining groups are booleans, all on by default: sharing, review, history, featureFlags, localization, audit, contextXray, userProfile, automation, docs, resources, web, workspaceApps, chat, and email. Use frameworkTools: "minimal" to turn every group off at once for a voice-first or single-purpose app, or frameworkTools: { preset: "minimal", resources: true } to keep just one — an explicit group key always wins over the preset.

Switching a group off removes it from the agent surfaces only: interactive chat, MCP, A2A, and background runs. The matching HTTP action routes stay mounted, because the UI reaches them through client hooks — disabling sharing must not 404 a share dialog that is still on screen. Weigh that parity cost before using it: the framework's contract is that anything the UI can do the agent can do, so sharing: false alongside a visible share button is a deliberate break, and only makes sense when the app has no such surface at all.

Framework tools are no longer promoted into the first model request by default, independent of this option. They stay in the searchable registry and load through tool-search, or immediately if the app names them in initialToolNames.

The top-level `databaseTools` and `extensionTools` options are deprecated in favor of `frameworkTools.database` and `frameworkTools.extensions`. They are still honored, but setting a top-level flag and its `frameworkTools` equivalent to conflicting values throws at plugin startup rather than booting with a tool surface nobody chose.

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:

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}

The ordinary deployed app agent works through app tools — actions, database, MCP, and configured integrations — rather than receiving ambient access to the app's source code. Source changes belong in the repository's development workflow. An embedded, local, or Builder frame can edit the repository when that frame is intentionally granted workspace and write tooling.

In a standard production deployment with 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 is a separate managed code-capable workflow where an agent can modify your app's UI in production. Connect your repo to Builder.io (free tier available) and prompt for UI changes directly — no redeploy needed.

How it works:

  1. Connect your agent-native repo to Builder.io (free tier available)
  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 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 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 and Portability.

What's next

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