PostgreSQL password. Also used to derive the unprivileged request-pool credential (see below) unless DATABASE_URL_APP or BREEZE_APP_DB_PASSWORD is set.
POSTGRES_DB
breeze
Database name
POSTGRES_PORT
5432
PostgreSQL port
DATABASE_URL
—
Auto
Full connection string (constructed from above in Docker)
DATABASE_URL_APP
—
One of these three (prod)
Explicit connection string for the unprivileged request role (breeze_app). Preferred, and required for multi-host / high-availability DATABASE_URL values, which cannot be derived automatically.
BREEZE_APP_DB_PASSWORD
—
One of these three (prod)
Password for the breeze_app role, used to derive its connection string from DATABASE_URL.
In production, Breeze runs request handlers as an unprivileged, row-level-security-enforced database role (breeze_app) rather than as the database owner. The API refuses to start unless it can configure that role — set one of DATABASE_URL_APP, BREEZE_APP_DB_PASSWORD, or POSTGRES_PASSWORD. This check runs whenever NODE_ENV=production, including when AUTO_MIGRATE=false. The API also refuses to start if it detects it is running as a SUPERUSER or BYPASSRLS role — if you were previously running as the postgres superuser, switch to a NOSUPERUSER/NOBYPASSRLS role. After doing so you may notice rows that were previously visible to a tenant “disappear”; that is row-level security working correctly, not data loss. See the request-database-role runbook.
Dedicated key that signs the pagination cursors returned by the Partner Export API. Must be canonical (standard) base64 — not base64url or hex — decoding to at least 32 bytes, and must not reuse JWT_SECRET. Generate: openssl rand -base64 32. The API refuses to start in production without it.
AGENT_ENROLLMENT_SECRET
—
Yes
Shared secret for agent enrollment. Generate: openssl rand -hex 32
APP_ENCRYPTION_KEY
—
Yes
AES encryption key for sensitive data at rest
APP_ENCRYPTION_KEY_ID
—
Optional identifier for the active encryption key (letters, digits, ./_/-). Setting it enables versioned, context-bound ciphertext for newly stored secrets and is required for the Microsoft 365 reset-password one-time credential reveal — without it the reset still succeeds but the temporary password is deliberately discarded rather than stored. Not present in the stock compose file: add it to .envand to the api service environment: block.
MFA_ENCRYPTION_KEY
—
Yes
Encryption key for MFA secrets
ENROLLMENT_KEY_PEPPER
—
Yes
HMAC pepper for enrollment key hashing
MFA_RECOVERY_CODE_PEPPER
—
Yes
HMAC pepper for recovery code hashing
ENROLLMENT_KEY_DEFAULT_TTL_MINUTES
60
Default enrollment key expiry
CHILD_ENROLLMENT_KEY_TTL_MINUTES
1440
Fresh TTL (minutes) given to a “child” enrollment key minted at installer-download, installer-link, or short-code redemption time, measured from mint time rather than the parent key’s remaining lifetime
INSTALLER_BOOTSTRAP_TOKEN_TTL_MINUTES
1440
Base TTL (minutes) for a freshly-issued installer bootstrap token when no explicit ttlMinutes is supplied. The token’s own expiry, not the parent enrollment key’s, governs how long an installer has to complete enrollment — see Enrollment Keys
INSTALLER_PARENT_MIN_REMAINING_SECONDS
60
Minimum remaining lifetime (seconds) a parent enrollment key must have to be used as an installer source. Guards against building an installer from a parent that expires before the download reaches the target machine
ENROLLMENT_KEY_CLEANUP_ENABLED
true
Set to false to disable the daily sweep that purges expired enrollment keys
ENROLLMENT_KEY_PURGE_AFTER_DAYS
7
Grace period (days past expiry) before an expired enrollment key is purged
SESSION_SECRET
—
Yes
Session signing secret
Access-token (15m) and refresh-token (7d) lifetimes are fixed in code
(apps/api/src/services/jwt.ts) — short-lived access tokens backed by
refresh-token rotation — and are intentionally not configurable via
deployment env vars.
Public API URL used for generated agent installers and shareable links. The bundled docker-compose.yml derives this from BREEZE_DOMAIN. If you use a custom compose file, you must set PUBLIC_API_URL in .envand map it into the api service environment: block, or Generate Link / Download Installer will fail with Server URL not configured.
API_URL
—
Legacy fallback for PUBLIC_API_URL, honored only by enrollment, installer, and MCP-invite code paths. Auto-update, dev-push, and a few other routes read PUBLIC_API_URL only — prefer setting PUBLIC_API_URL.
BREEZE_DOMAIN
—
Yes (prod)
Domain for Caddy TLS provisioning
ACME_EMAIL
—
Yes (prod)
Email for Let’s Encrypt certificate notifications
CORS_ALLOWED_ORIGINS
—
Comma-separated allowed CORS origins
IS_HOSTED
—
Yes (prod)
true for hosted SaaS edition, false for self-hosted. Must be set explicitly — the API refuses to boot otherwise. Controls signup gating, billing, and email-verification policy.
TRUST_PROXY_HEADERS
—
Yes (prod)
true when behind a reverse proxy (Caddy, Cloudflare). Must be set explicitly in production.
TRUSTED_PROXY_CIDRS
—
When TRUST_PROXY_HEADERS=true
Comma-separated CIDRs of trusted reverse proxies (e.g., 10.0.0.0/8,172.16.0.0/12). Required when proxy headers are trusted.
TRUST_CF_CONNECTING_IP
false
Set to trueonly when Breeze is fronted by Cloudflare. Cloudflare overwrites the CF-Connecting-IP header at its edge, but a non-Cloudflare reverse proxy (including the bundled Caddy) does not strip it — so trusting it without a Cloudflare front would let a client spoof the header to defeat IP allowlists and per-IP rate limits. When off, client IPs are resolved from X-Forwarded-For instead. If you are behind Cloudflare and leave this unset, client-IP attribution (rate limits, audit logs, IP allowlists) falls back to X-Forwarded-For; the API logs a startup warning in that case.
IP_ALLOWLIST_ENFORCEMENT_MODE
enforce
Partner dashboard IP allowlist mode. Use off only as a break-glass switch if an allowlist locks everyone out. The allowlist only enforces when a partner has entries configured and proxy trust is working through TRUST_PROXY_HEADERS plus TRUSTED_PROXY_CIDRS.
Public origin for customer-portal links in outbound email (e.g. invoice emails). Falls back to PUBLIC_APP_URL if unset. The portal is served under /portal on the main domain.
PORTAL_BASE_PATH
/portal
Base path the customer portal is served under. Baked into the portal image at build time — changing it requires rebuilding the portal image and keeping the Caddy /portal route in sync.
OFFBOARDING_DRAIN_WINDOW_HOURS
72
Grace window (hours, minimum 1) an organization stays in the offboarding state before it is finalized to churned. Setting an org’s status to offboarding locks its users out immediately but keeps its agents reachable for this window so a queued uninstall can actually be delivered and run; once the fleet drains or the window closes, remaining commands are cancelled and the tenant is closed out.
Push notifications to the iOS app go directly through Apple’s push service (APNs). All four values are optional but all-or-none — setting any one makes the other three required, so a half-configured setup fails at startup rather than silently at first send. Leave them all unset to disable push.
Variable
Default
Description
APNS_AUTH_KEY
—
Contents of the Apple .p8 push auth key
APNS_KEY_ID
—
Apple push key ID
APNS_TEAM_ID
—
Apple developer team ID
APNS_BUNDLE_ID
—
iOS app bundle identifier
APNS_ENVIRONMENT
production
production or sandbox. An empty value is treated as unset.
Comma-separated raw base64 Ed25519 public keys that sign release manifests. Use the published default for official Breeze releases (it is a public key, safe to commit); only change it if you sign your own binaries. The API refuses to start in production without it. BREEZE_RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS is accepted as an alias.
AGENT_AUTO_PROMOTE
true
Controls whether a newly registered/synced agent release immediately becomes the fleet upgrade target. true (default): publishing a release promotes it to the fleet at once. false: new binaries stay downloadable but the fleet only advances when you explicitly promote a version (platform admin + MFA) — see Controlled agent fleet rollout.
See Binary Distribution for details on local vs GitHub mode and S3 offloading.
The Breeze stack includes a coturn TURN server for WebRTC relay. Without TURN, remote desktop connections fail when either peer is behind symmetric NAT or a restrictive firewall.
Variable
Default
Required
Description
TURN_HOST
—
Yes (prod)
Public IP or hostname of the TURN server. Must be reachable by agents and viewers.
Retention for the raw device_metrics series, clamped to 1–365. History charts are served from metric_rollups and are unaffected. On first run after upgrading, older raw metrics are pruned — set this before starting the new version if you need more than 30 days.
The API samples its own event-loop delay so that a stalled main thread shows up
as a stalled main thread. Without it, a stall surfaces as unrelated downstream
errors — most confusingly a Postgres write CONNECT_TIMEOUT <host>:<port>, which
names the database even though the database and the network were both healthy.
The monitor runs by default; you do not need to set any of these.
Variable
Default
Description
EVENT_LOOP_MONITOR_INTERVAL_MS
1000
Sampling interval for event-loop delay. Values above EVENT_LOOP_STARVATION_WARN_MS leave a blind spot one interval wide — stalls shorter than one interval cannot be observed and are reported as cause unknown. The API logs a startup warning if you configure it that way.
EVENT_LOOP_STARVATION_WARN_MS
1000
Lag at or above which a reading counts as starvation. Drives the [event-loop] Main thread blocked… warning and the breeze_nodejs_eventloop_starved gauge.
EVENT_LOOP_MONITOR_DISABLED
—
Set to 1, true, yes, or on to turn the monitor off. With it off, every Postgres CONNECT_TIMEOUT is classified unknown, because starvation can be neither ruled in nor out.
At startup the API prints one line stating whether it can see its own loop and at
what settings — [event-loop] Lag monitor started (interval …ms, warn threshold …ms, CONNECT_TIMEOUT attribution threshold …ms), or a warning if the monitor is
disabled. The two thresholds can differ: CONNECT_TIMEOUT attribution caps its
own threshold at the 10s connect budget, so raising the warn threshold above 10s
cannot cause a timeout to be mis-attributed.
Lag is exported on /metrics/scrape (see Monitoring Stack).
It is deliberately not reported by /health/ready: that endpoint is
unauthenticated, and a live load gradient plus the configured threshold would let
an unauthenticated prober measure whether its own traffic is starving the
instance. Readiness is also not gated on lag — starvation is a load symptom, and
failing readiness under load would shift traffic onto peers and starve those too.
Both API and web Sentry integrations are off by default. Leave the DSN variables blank to disable. See Error Tracking & Privacy for what gets collected and how scrubbing works.
Variable
Default
Description
SENTRY_DSN
—
API Sentry DSN. Leave blank to disable server-side error tracking.
SENTRY_ENVIRONMENT
production
Sentry environment tag
SENTRY_RELEASE
—
Sentry release tag (e.g. git SHA)
SENTRY_TRACES_SAMPLE_RATE
0.1
Sentry performance trace sample rate (0.0-1.0)
SENTRY_PROFILES_SAMPLE_RATE
—
API performance-profiling sample rate (0.0-1.0). Leave blank to disable profiling.
PUBLIC_SENTRY_DSN_WEB
—
Web Sentry DSN. Leave blank to disable browser error tracking and on-error session replay. Inlined into the web bundle at build time.
SENTRY_AUTH_TOKEN
—
Sentry auth token used during the web build to upload source maps. If unset, source map upload is skipped and the build still succeeds.
Per-user and per-endpoint rate limits (login, MFA, dashboard usage) are fixed in code and not configurable via environment variables. The one deployment-tunable limit is the per-org agent cap, which bounds how much traffic a single tenant’s fleet can generate, even with thousands of agents.
Variable
Default
Description
AGENT_ORG_RATE_LIMIT_PER_MIN
600
Per-organization sliding-window rate limit on agent-authenticated endpoints. Returns 429 with Retry-After: 60 when exceeded. Sized for ~5 active agents per org; raise for MSPs with very large fleets. Not mapped in the stock compose file — to change it, add it to the api service environment: block as well as .env.
Maximum postgres-js connection pool size. Tune up if you see cascading 504s during heartbeat storms; confirm Postgres max_connections has headroom (default 100 is fine for a single API replica).
Required in production (and when DEPLOYMENT_ENV=staging). The API refuses to
boot without them, and the shipped compose files use ${VAR:?} so the container
stops before it starts. In development they fall back to the defaults shown.
compat accepts event-WebSocket tickets from pre-upgrade servers; enforce rejects them. Move to enforce ≥60s after the last old server stops. A malformed value falls back to enforce (fails closed).
REMOTE_WS_AUTH_MODE
post_upgrade
post_upgrade accepts remote-session tickets minted by pre-upgrade servers; pre_upgrade rejects them and applies full authorization before the WebSocket upgrade. Stay on post_upgrade until every legacy viewer token has expired.
REMOTE_ACCESS_ADMISSION_MODE
open
Remote-access barrier. closed returns 503 for every terminal/desktop/VNC/tunnel endpoint at the proxy. Used during the lease cutover and for maintenance.
REMOTE_WS_REDIS_TOPOLOGY
standalone-single-primary
Attestation that Redis is a single primary with AOF on, noeviction, cluster disabled. Only accepted value. Sentinel/Cluster/HA Redis will freeze remote admission by design.
REMOTE_WS_LEGACY_TICKET_WRITER_DRAINED_AT
—
Timestamp the last pre-upgrade ticket writer stopped. Used to compute the barrier-reopen deadline.
REMOTE_WS_LEGACY_VIEWER_ISSUER_DRAINED_AT
—
Timestamp the last legacy viewer-token issuer stopped. Gates the move to pre_upgrade.
OAUTH_AUTH_EPOCH_ENFORCE_AFTER
—
Required when MCP OAuth is enabled in production/staging. Absolute UTC timestamp ending the compatibility window for access tokens issued before the live-revocation release. Must be ≥1800s after new token writers start. Choose once; never extend on restart.
These ship disabled. Nothing changes on upgrade — each is a separate decision
with its own rollout gate. See Rollout Modes.
Variable
Default
Description
AGENT_MTLS_BINDING_MODE
off
audit counts certificate/device binding mismatches without denying; enforce denies them. Requires a validating proxy. Never inferred from NODE_ENV/IS_HOSTED/CF_MTLS_*.
MANAGED_SOFTWARE_POLICY_MODE
compat
enforce requires an upgraded agent for every managed-software command, public destinations included. Check your not-yet-upgraded device count first — they are denied outright.
AGENT_REQUIRE_MANIFEST_SIGNING_KEY_ID
false
Requires update manifests to carry a signing key ID. Not fleet-wide: older agent builds ignore the pushed value. Only enable once the missing-ID count has been zero for 7 consecutive days.
Serve the interactive Swagger UI and OpenAPI spec at /api/v1/docs. The UI loads its scripts and styles from unpkg, which is why it defaults off in production. (The former ENABLE_API_DOCS variable was never read and has been removed.)
USE_AGENT_SDK
—
Use Claude Agent SDK for AI chat
PORTAL_STATE_BACKEND
memory
Portal state backend: memory or redis (auto redis in production)
PUBLIC_ENABLE_EDR_INTEGRATIONS
false
Surface the EDR UI: the /security/edr fleet page, the device Security-tab EDR panel, and the Security dashboard summary cards. Set true at web image build time to enable.
PUBLIC_ENABLE_NETWORK_DEVICES_IN_LIST
false
Surface network-discovered assets alongside agent endpoints in the Devices list view. Set true at web image build time to enable.
Azure AD app (client) ID for multi-tenant M365 backup. When set with the secret below, enables one-click admin consent flow for connecting Microsoft 365 backups.
C2C_M365_CLIENT_SECRET
—
Azure AD app client secret for multi-tenant M365 backup
These enable partners to connect a Microsoft 365 shared support mailbox so customer email becomes tickets and replies are sent from that mailbox. This is a separate Azure app registration from the Cloud-to-Cloud Backup app above.
Variable
Default
Description
TICKET_MAILBOX_M365_CLIENT_ID
—
Azure AD app (client) ID for the Breeze Ticketing app. When set with the secret, the Microsoft 365 mailbox card appears under Settings → Ticketing → Inbound Email.
TICKET_MAILBOX_M365_CLIENT_SECRET
—
Client secret for the Breeze Ticketing Azure app.
PUBLIC_TICKET_MAILBOX_APP_ID
—
Build-time, browser-exposed app id shown in the Application Access Policy PowerShell snippet on the settings card. Safe to publish – it is the app’s public client id.
Customer Microsoft 365 Graph-read consent (optional)
This feature is off by default and safe to leave unset — every variable below stays empty in a normal deployment. The API only reads and validates them when you turn the feature on. Concretely, the API refuses to boot only when M365_CUSTOMER_GRAPH_READ_ONBOARDING_ENABLED=trueand one of the required variables is missing or malformed; while the flag is off (or absent) these variables are never parsed and any values you set are ignored.
Customer Graph-read lets a customer administrator grant read-only Microsoft Graph consent to Breeze’s dedicated multi-tenant application without handing you a reusable secret. The Entra certificate and Key Vault access live entirely inside an isolated executor sidecar — the API holds only an opaque, version-pinned reference and a private key it uses to sign short-lived internal requests to that sidecar. See Customer Graph Read for the admin-facing side.
Variable
Required when enabled
Format / example
Meaning
M365_CUSTOMER_GRAPH_READ_ONBOARDING_ENABLED
— (the switch)
true / false (default false)
Master switch for new consent initiation only. Existing connection status, retest, and disconnect flows are independent of it. When false, none of the variables below are validated.
M365_CUSTOMER_GRAPH_READ_ONBOARDING_ORG_IDS
Yes
Literal * or comma-separated canonical org UUIDs (e.g. * or 1111...,2222...)
Which Breeze organizations may start consent. * = all organizations; otherwise an allowlist of canonical UUIDs. Boot fails if enabled and this is empty.
M365_CUSTOMER_GRAPH_READ_CLIENT_ID
Yes
Canonical UUID
Entra application (client) ID of the shared multi-tenant Graph-read app.
M365_CUSTOMER_GRAPH_READ_CREDENTIAL_VERSION
Yes
Exactly 32 lowercase hex characters
Pins which stored credential version the executor must use. Must equal the version segment of M365_CUSTOMER_GRAPH_READ_VAULT_REF.
Opaque Key Vault reference to the app’s certificate material. The trailing version segment must matchM365_CUSTOMER_GRAPH_READ_CREDENTIAL_VERSION, or boot fails.
M365_GRAPH_READ_EXECUTOR_URL
Yes
Origin-only HTTPS, e.g. https://m365-executor.internal (no path, query, fragment, or userinfo)
Private base URL of the executor sidecar. Anything other than a bare https://host[:port] origin is rejected at boot.
M365_GRAPH_READ_EXECUTOR_AUDIENCE
Yes
Literal m365-graph-read-executor
JWT audience for the API→executor internal auth token. Must equal that exact string.
M365_GRAPH_READ_EXECUTOR_SIGNING_KID
Yes
Non-empty string
Key ID of the API’s signing JWK. If the JWK file carries a kid, it must match this value.
M365_GRAPH_READ_EXECUTOR_SIGNING_PRIVATE_JWK_FILE
Yes
Absolute path to a file, e.g. /run/secrets/m365_graph_read_executor_signing_private_jwk
The API’s Ed25519 (kty: OKP, crv: Ed25519) private signing JWK. The file must be a regular file whose mode denies all group/other access (0600 or stricter) and should be mounted read-only. Boot fails on a symlink, a relative path, over-broad permissions, or a JWK that isn’t a valid 32-byte Ed25519 key. Do not put the private JWK inline in an env var.
Customer Microsoft 365 Graph-actions (write) executor (optional)
Like Graph-read above, this feature is off by default and safe to leave unset — every variable below stays empty in a normal deployment. The API only reads and validates them when M365_GRAPH_ACTIONS_TOOLS_ENABLED=true; while the flag is off (or absent) these variables are never parsed and any values you set are ignored.
Where Graph-read is read-only inventory, Graph-actions authorises write operations against a customer’s Microsoft 365 tenant — the identity actions the AI can propose, such as resetting a user’s password, disabling a user, or changing mailbox rules. Those tools are Tier 3, so they still require human approval before anything executes (see AI Risk Engine → Approval workflow).
It uses the same isolation model as Graph-read: the Entra certificate and Key Vault access live entirely inside a separately deployed executor sidecar, and the API holds only an opaque version-pinned reference plus a private key it uses to sign short-lived internal requests.
Variable
Required when enabled
Format / example
Meaning
M365_GRAPH_ACTIONS_TOOLS_ENABLED
— (the switch)
true / false (default false)
Master switch for the M365 write-action tools. When false, none of the variables below are validated and the write tools are unavailable.
M365_GRAPH_ACTIONS_TOOLS_ORG_IDS
Yes
Literal * or comma-separated canonical org UUIDs
Which Breeze organizations may use M365 write actions. * = all organizations; otherwise an allowlist. Leave empty while the feature is off.
M365_CUSTOMER_GRAPH_ACTIONS_CLIENT_ID
Yes
Canonical UUID
Entra application (client) ID of the multi-tenant Graph-actions app.
M365_CUSTOMER_GRAPH_ACTIONS_CREDENTIAL_VERSION
Yes
Exactly 32 lowercase hex characters
Pins which stored credential version the executor must use. Must equal the version segment of M365_CUSTOMER_GRAPH_ACTIONS_VAULT_REF.
Absolute path, e.g. /run/secrets/m365_graph_actions_executor_signing_private_jwk
The API’s Ed25519 private signing JWK. Same rules as the Graph-read key: a regular file, mode 0600 or stricter, mounted read-only. Never inline the private JWK in an env var.
Self-hosted only. Point the AI Agent at an Anthropic-compatible /v1/messages backend (vLLM 0.23+, LiteLLM). Must be an http(s) URL. Permitted only when IS_HOSTED is explicitly false (or 0/no/off); refused at boot if IS_HOSTED is true, unset, or invalid.
ANTHROPIC_AUTH_TOKEN
—
Bearer token for the custom backend (use instead of ANTHROPIC_API_KEY when ANTHROPIC_BASE_URL is set).
ANTHROPIC_MODEL
claude-sonnet-4-6
Override the default model id for a raw vLLM backend. Leave unset with a LiteLLM gateway that aliases claude-sonnet-4-6 → your model.
Self-hosted: alternative Anthropic-compatible backends
A self-hosted operator can run the AI Agent against any backend that speaks the
Anthropic /v1/messages dialect — keeping full tool-use and the aiGuardrails
stack — by setting:
Terminal window
IS_HOSTED=false# required — ANTHROPIC_BASE_URL is refused otherwise
ANTHROPIC_BASE_URL=http://localhost:8000# your vLLM / LiteLLM endpoint
ANTHROPIC_AUTH_TOKEN=<backendbearertoken>
# Raw vLLM only — match the served model id (skip with a LiteLLM alias):
ANTHROPIC_MODEL=<served-model-id>
The recommended route is a LiteLLM gateway that aliases claude-sonnet-4-6
to your backend model, so no ANTHROPIC_MODEL override is needed. For an
OpenAI-dialect-only endpoint, use the separate MCP_LLM_* openai-compatible
path instead (chat-only, no tool-use).
ANTHROPIC_BASE_URL is fail-closed: it is honored only when self-host is
affirmatively declared (IS_HOSTED explicitly false/0/no/off). If
IS_HOSTED is true, unset, or set to an unrecognized value, the API refuses
to boot when ANTHROPIC_BASE_URL is present, and the value is also stripped
from the SDK subprocess environment — so a stray or unmapped IS_HOSTED can
never redirect platform AI traffic to a third-party endpoint.
Cost tracking is best-effort. The SDK can’t price a non-Anthropic model id, so
it reports total_cost_usd: 0; Breeze then falls back to token-based pricing,
and an unrecognized model id is priced at conservative Opus-tier
DEFAULT_PRICING ($5/$25 per MTok) — i.e. likely an over-estimate for a
cheap local model, not $0. For accurate accounting, add the served model id to
MODEL_PRICING in aiCostTracker.ts, or use the openai-compatible path’s
MCP_LLM_PRICE_* overrides.
Optional NIST NVD API key. Raises NVD request rate limits so vulnerability feed syncs run faster. Without it, syncs use the slower public rate; the feature works either way.