Guide

Environment variables

Agent Zero reads credentials and deployment policy exclusively from the environment. Endpoint URLs and credentials can never be named or embedded in .agent-zero.yml, so untrusted repository policy cannot redirect a secret.

Agent Zero reads credentials and deployment policy exclusively from the environment. Endpoint URLs and credentials can never be named or embedded in .agent-zero.yml, so untrusted repository policy cannot redirect a secret.

One env file per process

Each process reads exactly one .env, and that file sits next to the process that reads it. Nothing is shared implicitly: a file is loaded only by the process it belongs to, by that process's own tooling.

FileRead byLoaded byHolds
.envthe zero CLI (aube run zero …)Node's --env-file-if-exists=.env in the root scriptModel providers, subscription transports, GITHUB_TOKEN
apps/dashboard/.envthe dashboard: UI, control plane, webhooks, auth, mailNuxt, from beside nuxt.config.tsControl plane, webhook ingress, DATABASE_URL, authentication, mail, site URL
apps/docs/.envthe documentation siteNuxt, from beside nuxt.config.tsNUXT_APP_BASE_URL
packages/database/.envdrizzle-kit (db:generate, db:migrate)drizzle-kit, from its own working directoryDATABASE_URL

Each has a checked-in .env.example next to it; copy the ones you need:

cp .env.example .env
cp apps/dashboard/.env.example apps/dashboard/.env
cp apps/docs/.env.example apps/docs/.env
cp packages/database/.env.example packages/database/.env

Every file is optional. A deployment that sets real environment variables ships no .env at all, and real variables always win over the file.

A variable two processes both read is written in both files on purpose, rather than one process inheriting the other's configuration:

  • DATABASE_URL is read by the dashboard (the only process that opens the database) and by drizzle-kit when migrating. Point both at the same database — drizzle.config.ts and the server resolve it through the same function precisely so migrations cannot target one store while the dashboard opens another.
  • The model-provider keys are read by the CLI and, when the hosted control plane executes runs, by the dashboard.

server/auth.config.ts resolves DATABASE_URL at module load, so a dashboard started without it fails on the first request (missing required environment variable: DATABASE_URL) rather than degrading quietly.

Model providers

VariablePurpose
OPENAI_API_KEYCredential for openai (and legacy fallback for openai-compatible)
ANTHROPIC_API_KEYCredential for anthropic
GOOGLE_GENERATIVE_AI_API_KEYCredential for google
AI_GATEWAY_API_KEYCredential for ai-gateway (or Vercel OIDC)
OPENAI_COMPATIBLE_API_KEYCredential for openai-compatible
AGENT_ZERO_MODELDefault model name
AGENT_ZERO_MODEL_BASE_URLOperator-owned base URL for custom gateways and self-hosted endpoints

Each provider reads only its documented variable. See Model providers for the full matrix.

Control plane

The control-plane API (/rpc/** and /api/v1/**) fails closed: without AGENT_ZERO_CONTROL_PLANE_TOKENS every mutation is rejected while reads stay open.

VariablePurpose
AGENT_ZERO_CONTROL_PLANE_TOKENSComma-separated name:token bearer credentials
AGENT_ZERO_CONTROL_PLANE_REPOSITORIESComma-separated repository paths tasks.create may target
AGENT_ZERO_CONTROL_PLANE_MODESComma-separated name:mode|mode execution-mode grants; without one a principal may only request the non-writable observe and suggest modes
AGENT_ZERO_CONTROL_PLANE_ORIGINSComma-separated origins allowed to read /api/v1/** cross-origin via CORS; empty by default

See Protect endpoints for how these are enforced.

Webhooks

POST /webhooks/github fails closed (503, nothing ingested) until both variables are set.

VariablePurpose
GITHUB_WEBHOOK_SECRETHMAC secret for GitHub webhook authentication
AGENT_ZERO_CHECKOUT_PATHCheckout the webhook route binds incoming events to

Status publishing reads one fixed variable per provider (GITHUB_TOKEN, GITLAB_TOKEN, BITBUCKET_CLOUD_TOKEN, BITBUCKET_DATA_CENTER_TOKEN, GITEA_TOKEN) — see Source-control providers.

Authentication

Registration and GitHub OAuth are off until you turn them on, so a fresh deployment cannot be signed up for by a stranger.

VariableRequiredDefaultPurpose
NUXT_BETTER_AUTH_SECRETyesSession signing secret. Required under this name in production; BETTER_AUTH_SECRET is a development-only fallback
DATABASE_URLyesPostgres connection string; the pre-split AUTH_DATABASE_URL is still read when this is unset
AUTH_ENABLE_SIGNUPnofalseSet to true to allow self-registration
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETnoEnables the GitHub button when both are set
AUTH_ENABLE_ORGANIZATIONSnofalseEnables organizations; requires a working mail transport
AUTH_ALLOW_ORGANIZATION_CREATIONnofalseWhether any signed-in user may create an organization
AUTH_ENABLE_DEVICE_AUTHORIZATIONnofalseSet to true to let zero login obtain a session through the RFC 8628 device flow
OAUTH_PROXY_PRODUCTION_URL / OAUTH_PROXY_SECRETnoRoutes a preview or local origin's OAuth round trip through production; both required, and the secret must match across environments
NUXT_PUBLIC_SITE_URLnoBase URL override for a custom domain or deterministic OAuth callbacks; auto-detected from the request otherwise

Hosted infrastructure

Cloud-managed deployments only. @better-auth/infra's sentinel() and dash() plugins are registered only when all three are set; a self-hosted install leaves them empty and gets neither.

VariableRequiredDefaultPurpose
BETTER_AUTH_API_URLnoDash API origin
BETTER_AUTH_KV_URLnoKV service origin; also what the browser-side sentinel client identifies visitors against
BETTER_AUTH_API_KEYnoProject API key
Third-party data flowSetting these opts the deployment into a service outside it. sentinel() reports sign-in and sign-up attempts for scoring, and the browser-side sentinel client fingerprints every visitor and identifies them against BETTER_AUTH_KV_URL — which is why that client plugin is loaded only when these variables are configured.dash() mounts roughly 79 endpoints under /api/auth/dash/**, including execute-adapter, impersonate-user, delete-many-users, and export-users. All but two require a JWT signed by the hosted service, verified against its JWKS with a five-minute maximum age, whose apiKeyHash claim must also match a hash of BETTER_AUTH_API_KEY — so controlling the API origin alone does not admit a caller.
Account creation bypasses your sign-up policydash()'s accept-invitation and complete-invitation endpoints cannot carry that JWT guard, because the invitee holds no API key. They are authorized by an invitation token validated against the hosted API, and they create a user with emailVerified: true, optionally a password account, and a session — through the internal adapter, so they bypass both AUTH_ENABLE_SIGNUP and the invitation flow gated by AUTH_ENABLE_INVITATIONS.In other words: enabling dash() delegates account creation in your database to whoever can mint an invitation in the hosted console. Treat access to that console as equivalent to AUTH_ENABLE_SIGNUP=true.
Build-time captureThe sign-in methods the login page offers are derived at build time from the same policy variables the server reads at runtime. Whenever you change AUTH_ENABLE_SIGNUP, the GitHub OAuth credentials, or the BETTER_AUTH_* hosted-infrastructure variables, rebuild the app, or the login page will keep advertising the old capabilities (the server still enforces its own policy either way).

Mail

console logs instead of delivering and is the default when neither MAIL_PROVIDER nor RESEND_API_KEY is configured, so an unconfigured deployment cannot silently attempt real delivery. If MAIL_PROVIDER is absent, a Resend credential selects Resend automatically; an explicit provider always wins.

VariablePurpose
MAIL_PROVIDEROptional explicit console, resend, or smtp selector
MAIL_FROMSender address
RESEND_API_KEYRequired for Resend; also selects it when MAIL_PROVIDER is absent
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORDRequired when MAIL_PROVIDER=smtp
SMTP_SECUREImplicit TLS: true on 465, false on 587

See Mails.

Dashboard

VariablePurpose
PORTDashboard port (default 3000)

Deployment target

Build-time only: these pick the deployment preset the dashboard build emits, and with it the KV driver its task history resolves. Unset means the self-hosted node-server bundle in .output/.

VariablePurpose
NITRO_PRESETDeployment target as the Nitro preset each plan pins: vercel, node-server, cloudflare-module, deno-deploy, netlify
VITEHUB_HOSTINGThe same target as a ViteHub plan name: vercel, node, cloudflare, deno, netlify; takes precedence over NITRO_PRESET
KV_REST_API_URL / KV_REST_API_TOKENUpstash credentials the KV store reads at runtime on a host without a writable filesystem

Each variable is matched against its own vocabulary and nothing else: a value ViteHub would refuse to build under — vercel-edge or a bare node in NITRO_PRESET, say — is rejected up front rather than resolved to the neighbouring target.

See Deployment.

Build metadata

Every Nuxt app resolves what build it is — version, commit, branch, deploy channel, deploy URL — and publishes it under runtimeConfig.public.buildInfo, read with useBuildInfo(). The resolution lives in packages/build-env and runs in two passes.

The first pass runs during the build. It asks the hosting provider first and the checkout second: a provider knows the branch a detached CI checkout cannot name, and knows whether the deploy is production. VERCEL_GIT_COMMIT_SHA and friends on Vercel, COMMIT_REF and CONTEXT on Netlify, CF_PAGES_COMMIT_SHA on Cloudflare Pages, GITHUB_SHA on a GitHub Actions runner. None of these are set by you; they are set by the platform, and are listed in turbo.jsonc so a build never restores a cached bundle that reports a different commit.

The second pass runs in the deployed server, and is what every target that is not Vercel needs. A container image built in CI has none of the platform variables in scope while it is being built, so its first pass resolves what git can tell it and leaves the rest as unknown. Those fields — and only those — are completed when the server starts, from the environment the host actually runs it in. A field the build resolved is never overwritten: the commit a bundle was compiled from is a property of the bundle, not of the machine serving the request.

For a deployment on no recognised platform, or one that wants to state outright what it is:

VariablePurpose
AGENT_ZERO_BUILD_COMMITFull commit SHA the bundle was built from
AGENT_ZERO_BUILD_BRANCHGit branch
AGENT_ZERO_BUILD_PR_NUMBERPull request number, for a pull-request deploy
AGENT_ZERO_BUILD_URLURL of this deploy
AGENT_ZERO_BUILD_PRODUCTION_URLURL of the production domain
AGENT_ZERO_BUILD_ENVDeploy channel: dev, preview, canary, or release

Setting any of them takes precedence over every auto-detected platform, so an operator's answer always wins over an inferred one. AGENT_ZERO_BUILD_ENV is the field detection can never work out on its own: a self-hosted staging deployment is a preview in every way that matters to the people looking at it, and nothing about a plain node .output/server/index.mjs says so.

Individual fields can also be overridden through Nuxt's own public runtime config channel — NUXT_PUBLIC_BUILD_INFO_COMMIT, NUXT_PUBLIC_BUILD_INFO_BRANCH, and so on — which applies before the server's own pass and needs no rebuild. There is no _SHORT_COMMIT variant: the dashboard derives the abbreviated form from commit wherever it is displayed, rather than declaring it as its own field that could drift from the commit it abbreviates.

A prerendered route has no server left to ask, so on apps/marketing the second pass runs while the page is prerendered rather than while it is served. On Vercel that changes nothing: the build already resolved every field.

Documentation

VariablePurpose
NUXT_APP_BASE_URLBase path (default /; use /<repository>/ on Pages)
Copyright © 2026