Skip to content

Environment Variables

All configuration is done through environment variables, defined in your .env.prod file. This page documents every variable.

Variable Default Required Description
POSTGRES_USER breeze PostgreSQL username
POSTGRES_PASSWORD — Yes 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 for direct (non-Compose) deployments and for deploy/docker-compose.prod.yml (managed Postgres), and required for multi-host / high-availability DATABASE_URL values, which cannot be derived automatically. Leave it empty on the bundled root docker-compose.yml — see the callout below.
BREEZE_APP_DB_PASSWORD — One of these three (prod) Password for the breeze_app role, used to derive its connection string from DATABASE_URL.
BREEZE_ALLOW_UNSAFE_DB_ROLE — Local development escape hatch. Set to true to start even when the request role is SUPERUSER/BYPASSRLS, printing a loud warning banner every boot. Ignored when NODE_ENV=production, and never appropriate for a deployment holding real data.

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 configuration requirement applies when NODE_ENV=production, including when AUTO_MIGRATE=false. Separately, the API verifies the capabilities of the role its request pool actually connected as on every startup, in every environment — NODE_ENV does not switch that check off — and refuses to start if that role is SUPERUSER or BYPASSRLS — 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.

Database connection prologue and wedged-backend recovery

Section titled “Database connection prologue and wedged-backend recovery”
Variable Default Required Description
DB_ACCESS_CONTEXT_PROLOGUE_TIMEOUT_MS 15000 Timeout for the connection-setup work Breeze runs at the start of each request’s database access (setting the row-level-security context). The request work that follows is never bounded by it, and neither is the wait for a free pool connection before it (see DB_POOL_ACQUIRE_TIMEOUT_MS). 0 disables the timeout; positive values below 1000 are clamped to 1000.
DB_POOL_ACQUIRE_TIMEOUT_MS 10000 How long a request waits for a free database pool connection before failing with DbPoolAcquireTimeoutError. Hitting it means the pool is saturated (every connection busy) or the API is CPU-starved, not that a connection is stuck, so it does not trigger wedged-backend reclamation. A connection that frees up after the timeout is returned to the pool unused. 0 disables the timeout (requests queue indefinitely); positive values below 1000 are clamped to 1000. A request still waiting for a permit when the timeout hits never reaches the database. The default keeps this timeout plus DB_ACCESS_CONTEXT_PROLOGUE_TIMEOUT_MS and two DB_TIMER_LAG_GRACE_MS periods at 29 s, under the agent’s 30 s HTTP timeout, for a request that opens one database context. A request whose work escalates into a second context (a nested system-scope or re-resolved transaction) gets a second acquire and setup budget on top, so its worst case is longer.
DB_TIMER_LAG_GRACE_MS 2000 When the pool-acquire or connection-setup timer fires at least 1 s late because the API was overloaded, it gets this one extra grace period so a database reply that already arrived is not discarded. 0 disables; capped at 10000.
DB_WEDGED_BACKEND_SCAN_DISABLED unset (scanning on) Set to disable the periodic scan that detects database connections stuck at session start. When the scan finds a connection stuck on the session-setup statement for at least 5 minutes, it requests a reclamation pass (subject to all the reclamation limits below).
DB_WEDGED_BACKEND_SCANNER_RECLAIM_DISABLED unset (scanner reclaim on) Set to make the periodic scan report stuck connections without requesting reclamation. Reclamation of permits still held by abandoned transactions (one setup budget after the timeout) still runs unless DB_WEDGED_BACKEND_RECLAIM_DISABLED is set.
DB_WEDGED_BACKEND_SCAN_INTERVAL_MS 300000 (5 min) How often the wedged-backend scan runs.
DB_WEDGED_BACKEND_MIN_AGE_MS 300000 (5 min) How long a backend must have been stuck before the scan reports it.
DB_WEDGED_BACKEND_RECLAIM_DISABLED unset (reclamation on) Set to detect wedged backends without terminating them. With reclamation disabled, a pool permit held by a transaction abandoned at its session-setup timeout stays held for as long as its backend stays wedged, so the effective pool size drops by one per wedged backend (the API logs [db-pool-admission] warnings and DbPoolPermitsHeldByAbandonedTransactions fires). Deferred after-transaction work registered inside that abandoned transaction (runAfterDbContextExit) never runs.
DB_WEDGED_BACKEND_RECLAIM_MIN_INTERVAL_MS 60000 (1 min) Minimum time between reclamation passes.
DB_WEDGED_BACKEND_RECLAIM_MAX_PER_PASS 4 Maximum number of wedged backends reclaimed in a single pass.
DB_WEDGED_BACKEND_RECLAIM_TIMEOUT_MS 5000 Timeout for the reclamation connection itself.
DB_WEDGED_BACKEND_CONFIRM_DELAY_MS 250 Delay before confirming a backend is still wedged prior to reclaiming it — a backend is only reclaimed once two consecutive snapshots agree it hasn’t moved.

Together, these detect and reclaim database connections stuck at session start (before they ever reach request work) — reclamation opens a fresh connection and signals the wedged one so its pool slot can reconnect, rather than leaving it to block a connection permanently. Defaults come from .env.example; leave them alone unless you’re troubleshooting a specific incident.

Variable Default Required Description
REDIS_URL redis://localhost:6379 Yes (prod) Redis connection URL. Must carry a password in production (e.g., redis://:password@localhost:6379) — the API refuses to boot if it doesn’t.
REDIS_PORT 6379 Redis port
REDIS_PASSWORD — Yes (prod) Password for Redis authentication. Set in docker-compose and include in REDIS_URL.
REDIS_PASSWORD_FILE — Path to a file containing the Redis password (Docker secret style). Used in place of REDIS_PASSWORD when secrets are mounted as files.
Variable Default Required Description
JWT_SECRET — Yes JWT signing key (min 32 chars). Generate: openssl rand -base64 64
PARTNER_API_CURSOR_SIGNING_KEY — Yes (prod) 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 — Identifier for the active encryption key (letters, digits, ./_/-). Setting it enables versioned, context-bound ciphertext for newly stored secrets. The API refuses to start without it whenever M365_GRAPH_ACTIONS_TOOLS_ENABLED=true (since v0.106.0 — the write-action reveal path seals its temporary credential with key-bound ciphertext and fails closed) or BREEZE_ROLE is api or worker (the worker split encrypts commands relayed between the two processes). From v0.113.0, every other deployment gets a boot-time warning instead of a refusal when it is unset, and saving integration credentials fails with 503 until it is set: the Discord, Teams, Grafana, OpsGenie, PagerDuty, monitoring-webhook, ticketing and PSA settings pages seal each credential with key-bound ciphertext and refuse to fall back to unbound ciphertext. Existing installs whose guided-setup.sh run predates this key keep running otherwise; generate any [A-Za-z0-9._-]+ identifier (changing it later does not rotate APP_ENCRYPTION_KEY). Not present in older compose files: add it to .env and to the api (and worker) service environment: block.
MFA_ENCRYPTION_KEY — Yes Encryption key for MFA secrets
ENROLLMENT_KEY_PEPPER — Yes Pepper for enrollment key and installer token hashing. Rotating it invalidates all outstanding installer links (no legacy-pepper fallback, unlike enrollment keys)
MFA_RECOVERY_CODE_PEPPER — Yes HMAC pepper for recovery code hashing
ENROLLMENT_KEY_DEFAULT_TTL_MINUTES 43200 (30 days) Default enrollment key expiry. Every path that mints a key shares this fallback — the Add Device modal, the Partner API, and installer child keys
CHILD_ENROLLMENT_KEY_TTL_MINUTES inherits ENROLLMENT_KEY_DEFAULT_TTL_MINUTES 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 10080 Base TTL (minutes) for a freshly-issued installer bootstrap token when no explicit ttlMinutes is supplied. Default 7 days; maximum 43200 (30 days) — a larger value is clamped, with a warning at API startup. Tokens already issued keep their original expiry. 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
BREEZE_BOOTSTRAP_ADMIN_EMAIL — Yes (prod, first boot) Email for the initial admin account, created automatically the first time the API starts against an empty users table. Required in production — without it, that first-boot auto-seed throws and the API crash-loops. Must not be the dev default (admin@breeze.local).
BREEZE_BOOTSTRAP_ADMIN_PASSWORD — Yes (prod, first boot) Password for the initial admin account. Required in production, must be at least 16 characters, and must not be the dev default or look like a placeholder (changeme, password, change-in-production, etc.) — the API refuses to start otherwise. Generate a one-time secret (openssl rand -base64 24) and rotate it after first login.
BREEZE_BOOTSTRAP_ADMIN_NAME Bootstrap Admin (prod) / Breeze Admin (dev) Display name for the initial admin account.
BREEZE_AUTHENTICATOR_ATTESTATION_ENFORCED true Whether an approver device must have server-verified platform attestation before it counts as a level-4 authenticator for the most sensitive approvals. On by default — leave it alone unless you have a documented reason. Set it to false only to accept a device’s own unverified claim that it is platform-bound. This is a fail-closed gate: an unrecognized value logs a warning and keeps enforcement on, and the API refuses to boot on a malformed value rather than silently disabling it.
APPROVER_ASSURANCE_DEFAULT_ENFORCE_FROM 2026-11-05T00:00:00Z Date from which partners that have not saved an Approval Security enforcement choice require a registered approver device for high- and critical-risk approvals (the platform default). Accepts an ISO date or timestamp; an unparseable value refuses to boot. Partners that saved Required or Not required are unaffected.
APPLE_APP_ATTEST_APP_ID D8W6N2JYMA.com.breeze.rmm Apple’s <TeamID>.<bundle id> App Attest identifier, checked when an iPhone registers as a level-4 approver device. It’s hashed into the attestation, so a wrong value rejects every genuine attestation rather than accepting a foreign one. Only override it if you build your own signed iOS app.
APPLE_APP_ATTEST_ENVIRONMENT production Which App Attest environment’s attestations are accepted. Only the exact value development opts into accepting development-build attestations; anything else (a typo, a stray space) is treated as production and logs a warning — the wrong value can only ever reject a genuine attestation, never admit a forged one.
PARTNER_API_ENROLLMENT_KEY_MAX_TTL_MINUTES 525600 (365 days) Ceiling on the lifetime a caller may request when creating an enrollment key through POST /partner-api/v1/enrollment-keys
AGENT_ENROLL_RATE_LIMIT 10 Agent enrollments allowed per source IP per window on POST /api/v1/agents/enroll. Machines behind one NAT address share the bucket, so raise it before enrolling a site with more than ~10 machines at once. Must be a positive integer (the API refuses to boot otherwise)
AGENT_ENROLL_RATE_WINDOW_SECONDS 60 Window, in seconds, for AGENT_ENROLL_RATE_LIMIT
PARTNER_API_ENROLLMENT_KEY_WRITE_RATE_LIMIT 10 Enrollment-key creations per hour per service principal on the Partner API
PARTNER_API_ENROLLMENT_KEY_WRITE_PARTNER_RATE_LIMIT 100 Enrollment-key creations per hour per partner, across all of that partner’s service principals
WEBAUTHN_RP_ID derived from PUBLIC_APP_URL Set only when the browser reaches the app on a different host than PUBLIC_APP_URL (for example WEBAUTHN_RP_ID=localhost on a local stack). Without a correct value, every passkey ceremony fails with “The RP ID … is invalid for this domain.”
WEBAUTHN_ORIGIN derived from PUBLIC_APP_URL Companion to WEBAUTHN_RP_ID — set together when passkeys are served from a non-default host.
WEBAUTHN_RP_NAME Breeze RMM Display name shown to the user during a passkey registration/sign-in ceremony.

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.

Variable Default Required Description
NODE_ENV production Environment mode
API_PORT 3001 API server port
WEB_PORT 4321 Web dashboard port
BREEZE_API_ROLE all Which work the api container takes on. all (the default) keeps the single-container behaviour: the API serves requests, holds agent connections, and runs every background job itself. Set it to api only when you also run a separate worker container, so the two do not both run the same scheduled jobs. See Worker Split.
BREEZE_ROLE all The role the process itself runs as. You normally set BREEZE_API_ROLE instead and let the compose file map it; the optional worker container fixes this to worker for you.
PUBLIC_API_URL https://${BREEZE_DOMAIN} (bundled compose only) 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 .env and 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. Also the default source for PUBLIC_API_URL and PUBLIC_PORTAL_URL in the bundled compose file, so it’s required even when you front TLS yourself.
ACME_EMAIL — Yes (prod) Email for Let’s Encrypt certificate notifications
CADDY_LOCAL_CERTS — (empty) Set to the literal local_certs when BREEZE_DOMAIN is a domain Let’s Encrypt cannot reach — an internal name that only resolves on a LAN/VPN, or a host whose ports 80/443 are closed to the internet. Caddy then issues from its own internal CA instead of failing the ACME order and aborting the TLS handshake, which browsers report as ERR_SSL_PROTOCOL_ERROR with no warning to click through. Browsers warn until the root from docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt is trusted on each client. Leave empty for any internet-reachable deployment; not needed for localhost or a bare IP, which Caddy already self-signs.
CORS_ALLOWED_ORIGINS — Comma-separated allowed CORS origins. Requests the browser sends from the same origin as the API — including a browser reaching Breeze through an SSH tunnel or on a LAN address — are accepted whether or not that origin is listed, so you only need this for a genuinely cross-origin front end.
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, email-verification policy, and the default for signup-abuse detection.
ABUSE_SIGNALS_ENABLED value of IS_HOSTED Signup-abuse detection switch. Defaults to the value of IS_HOSTED — on for a hosted deployment, off for a self-hosted one (its heuristics police untrusted public signups and are noise on a single-team install; when off, the only artifact is one [AbuseSignals] Disabled line at boot). Set true to opt a genuinely multi-tenant self-hosted service in, or false to switch a hosted deployment off. When set, it must be a boolean (true/false, 1/0, yes/no, on/off) — validated at boot.
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 true / false, 1 / 0, yes / no, on / off (empty or unset is allowed) Set to true only 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. From v0.106.0 a value outside the list above stops the API from starting — previously a typo here was silently read as “off”, quietly attributing every client IP to the wrong address.
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. Only enforce, off or an empty/whitespace-only value (treated as unset) are accepted — anything else refuses startup rather than silently enforcing while you believe enforcement is off.
DASHBOARD_URL — URL for links in emails
PUBLIC_APP_URL — Public-facing app URL
PUBLIC_PORTAL_URL https://${BREEZE_DOMAIN}/portal (bundled compose only) 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.
Variable Default Description
EMAIL_PROVIDER auto Provider: auto, resend, smtp, or mailgun
RESEND_API_KEY — Resend API key
EMAIL_FROM noreply@breeze.local Sender address
SMTP_HOST — SMTP server hostname
SMTP_PORT 587 SMTP port
SMTP_USER — SMTP username
SMTP_PASS — SMTP password
SMTP_FROM noreply@breeze.local SMTP-specific sender address
SMTP_SECURE false Use TLS for SMTP
MAILGUN_API_KEY — Mailgun API key
MAILGUN_DOMAIN — Mailgun sending domain
MAILGUN_BASE_URL https://api.mailgun.net Mailgun API base URL
MAILGUN_FROM noreply@breeze.local Mailgun-specific sender address
SMTP_TIMEOUT_MS 30000 How long an outbound SMTP send may take before it is abandoned. Applies to all outbound mail. Without a bound, a wedged mail host can hold a send open for nodemailer’s ten-minute default
MAILGUN_TIMEOUT_MS 120000 The same bound for the Mailgun HTTP transport

Sending customer-facing mail from a partner’s own domain, rather than from EMAIL_FROM, is set up separately — see Custom sender addresses for the EMAIL_DOMAINS_* variables and the three ways to configure them.

Variable Default Description
TWILIO_ACCOUNT_SID — Twilio Account SID
TWILIO_AUTH_TOKEN — Twilio Auth Token
TWILIO_VERIFY_SERVICE_SID — Twilio Verify service SID (for SMS MFA)
TWILIO_MESSAGING_SERVICE_SID — Twilio Messaging Service SID (for alert SMS)
TWILIO_PHONE_NUMBER — Twilio phone number for outbound SMS

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.

Push notifications to the Android app go through Firebase Cloud Messaging.

Variable Default Description
FIREBASE_SERVICE_ACCOUNT — Firebase service-account credentials JSON for the project that owns the Android app. Optional — leave it unset and Android devices simply receive no push notifications; nothing else changes and the API still boots. iOS push is unaffected either way.
PLAY_INTEGRITY_SERVICE_ACCOUNT — Google service-account credentials for decoding Play Integrity verdicts when an Android phone registers as an approver device for high-assurance approvals. Accepts the raw JSON or a base64-encoded copy of it. Optional: leave it unset and Android approver registration is verified by hardware Key Attestation alone.
Variable Default Required Description
BINARY_SOURCE github Download source: local (serve from disk, optional S3) or github (redirect to GitHub Releases)
BINARY_EDITION self-host Agent edition this deployment serves: self-host (default — today’s behavior, leave unset on self-hosted installs) or hosted. In production, hosted requires BINARY_SOURCE=local and an explicit RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS, or the API refuses to boot. An unrecognized value logs a warning and falls back to self-host.
BINARY_GITHUB_REPOSITORY lanternops/breeze GitHub owner/repository used for all release consumption in github mode — download redirects, release sync, and manifest validation. Shape-validated at boot in every environment (owner/repository; no ./.. segments). Point it at your signing repo to serve self-signed packages (see Sign Your Own Agent Packages). In production, overriding it requires RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS to be explicitly set to your manifest key — leaving the official Breeze key as the only trust root while pointing at a non-official repo is refused at boot (every sync would otherwise fail closed and silently freeze your fleet).
AGENT_BINARY_DIR ./agent/bin Local directory containing agent binaries
VIEWER_BINARY_DIR ./viewer/bin Local directory containing viewer installers
HELPER_BINARY_DIR AGENT_BINARY_DIR Local directory containing helper (Breeze Assist) installers. When unset it uses the same directory as AGENT_BINARY_DIR (v0.120.0+; earlier releases used ./agent/bin). The bundled Docker Compose sets this to /data/binaries/helper. With S3 configured, it is synced to the helper/ prefix on startup.
GITHUB_REPO — Deprecated alias for BINARY_GITHUB_REPOSITORY, kept so a deployment that set it before the release-source unification does not silently revert to the official repo on upgrade. It is honoured only when BINARY_GITHUB_REPOSITORY is unset, logs a deprecation warning, and counts as an override for the production key requirement below. Migrate to BINARY_GITHUB_REPOSITORY.
BINARY_VERSION_FILE — Path to VERSION file for local mode DB registration (set automatically in Docker Compose)
BINARY_VERSION — Release tag used for both the download redirect and boot-time GitHub sync (falls back to BREEZE_VERSION, then latest). Point it at the newest release that is actually published on GitHub — a pin to a tag with no published release registers no agent binaries at all, and does not fall back to latest.
RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS yzx8ftmcls6uBetFC5SYnZhBo+cbur3IX50TbBthTso= (official releases) Yes (prod) 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. When BINARY_GITHUB_REPOSITORY points at your own signing repository, this must be your own manifest key (the workflow run summary prints it). 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): syncing a release promotes it to the fleet at once. Boot-time sync is pinned to the release matching your own BREEZE_VERSION (or BINARY_VERSION), so a newly published GitHub release does not move your fleet until you upgrade your server images. 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.

Variable Default Description
S3_ENDPOINT — S3-compatible endpoint (MinIO, R2, AWS). Uses path-style addressing.
S3_ACCESS_KEY — Access key
S3_SECRET_KEY — Secret key
S3_BUCKET — Bucket name
S3_REGION us-east-1 Bucket region
S3_PRESIGN_TTL 900 Presigned URL expiration in seconds (15 min)
MINIO_API_PORT 9000 MinIO API port (Docker only)
MINIO_CONSOLE_PORT 9001 MinIO web console port (Docker only)

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 — Only with the turn profile Public IP or hostname of the TURN server. Must be reachable by agents and viewers.
TURN_PORT 3478 TURN listening port (UDP and TCP)
TURN_SECRET — Only with the turn profile Shared secret for TURN credential generation. Generate: openssl rand -hex 32. Not needed to bring the stack up: the bundled Compose file only starts coturn under the turn profile, and when it is enabled coturn refuses to start on an empty secret rather than running as an open relay. The value is mounted as a file rather than passed on the command line.
TURN_REALM breeze.local TURN authentication realm

From v0.118.0 each user may request 30 TURN credentials per 10 minutes. Tune it with TURN_CREDENTIAL_MINT_LIMIT_PER_WINDOW and TURN_CREDENTIAL_MINT_WINDOW_SECONDS (see Agent traffic and ingest budgets).

Variable Default Description
METRICS_SCRAPE_TOKEN — Bearer token for /metrics/scrape. The same token also guards the optional worker container’s own /metrics endpoint — a separate scrape target. See Worker Split.
METRICS_INCLUDE_ORG_ID false Include org IDs in Prometheus labels
METRICS_SCRAPE_IP_ALLOWLIST — Restrict metrics scraping by IP
LOG_LEVEL info Log verbosity: debug, info, warn, error
LOG_JSON true Structured JSON logging
GRAFANA_ADMIN_USER admin Grafana admin username
GRAFANA_ADMIN_PASSWORD — Grafana admin password
GRAFANA_PORT 3000 Grafana web UI port (monitoring stack)
GRAFANA_ROOT_URL http://localhost:3000 Grafana public root URL (monitoring stack)
PROMETHEUS_PORT 9090 Prometheus web UI port (monitoring stack)
ALERTMANAGER_PORT 9093 Alertmanager web UI port (monitoring stack)
LOKI_PORT 3100 Loki log aggregation API port (monitoring stack)
DEVICE_METRICS_RETENTION_DAYS 30 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.

Breeze prunes its own operational tables nightly so a long-running instance does not accumulate delivery queues and diagnostic history forever. Every one of these is optional — the defaults are sensible and nothing needs setting on a normal deployment. Each sweep runs in batches so it never holds a long transaction; raise *_MAX_BATCHES only if a sweep repeatedly stops before it has caught up.

Variable Default Description
TICKET_OUTBOX_RETENTION_DAYS 14 Days to keep delivered or exhausted ticket-email delivery records, clamped to 1–365
TICKET_OUTBOX_RETENTION_BATCH_SIZE 5000 Rows deleted per batch
TICKET_OUTBOX_RETENTION_MAX_BATCHES 50 Maximum batches per nightly run
INTENT_OUTBOX_RETENTION_DAYS 14 Days to keep delivered approval/action-intent delivery records, clamped to 1–365
INTENT_OUTBOX_RETENTION_BATCH_SIZE 5000 Rows deleted per batch
INTENT_OUTBOX_RETENTION_MAX_BATCHES 50 Maximum batches per nightly run
METRIC_ANOMALY_INCIDENT_RETENTION_DAYS 14 Days to keep resolved metric-anomaly incidents, clamped to 1–365
METRIC_ANOMALY_INCIDENT_RETENTION_BATCH_SIZE 5000 Rows deleted per batch
METRIC_ANOMALY_INCIDENT_RETENTION_MAX_BATCHES 50 Maximum batches per nightly run
AGENT_LOG_RETENTION_DAYS 7 Days to keep uploaded agent diagnostic logs, clamped to 1–365
AGENT_LOG_RETENTION_BATCH_SIZE 10000 Rows deleted per batch
AGENT_LOG_RETENTION_MAX_BATCHES 200 Maximum batches per nightly run
AI_INVOCATIONS_RETENTION_DAYS 400 Days to keep AI invocation ledger rows, capped at 3650
AI_INVOCATIONS_RETENTION_BATCH_SIZE 5000 Rows deleted per batch
AI_INVOCATIONS_RETENTION_MAX_BATCHES 200 Maximum batches per daily run
CHANGE_LOG_RETENTION_DAYS 90 Days to keep processed device change-log entries, clamped to 1–365
CHANGE_LOG_RETENTION_BATCH_SIZE 10000 Rows deleted per batch
CHANGE_LOG_RETENTION_MAX_BATCHES 200 Maximum batches per nightly run
IP_HISTORY_RETENTION_DAYS 90 Days to keep device IP-address history, clamped to 1–365
IP_HISTORY_RETENTION_BATCH_SIZE 10000 Rows deleted per batch
IP_HISTORY_RETENTION_MAX_BATCHES 200 Maximum batches per nightly run
SNMP_METRICS_RETENTION_DAYS 7 Days to keep raw SNMP-polled metrics, clamped to 1–365
SNMP_METRICS_RETENTION_BATCH_SIZE 10000 Rows deleted per batch
SNMP_METRICS_RETENTION_MAX_BATCHES 200 Maximum batches per nightly run
PROCESS_SAMPLE_RETENTION_DAYS 7 Days to keep per-process monitoring samples, clamped to 1–14. Not batch/max-batches tunable — this sweep loops in fixed 10,000-row batches until caught up.
SERVICE_PROCESS_CHECK_RESULTS_RETENTION_DAYS 14 Days to keep service/process watch check results, clamped to 1–365. Not batch/max-batches tunable — same fixed-batch-loop shape as the process-sample sweep above.
RELIABILITY_HISTORY_RETENTION_DAYS 120 Days to keep device reliability-scoring history, floored at 30
RELIABILITY_HISTORY_RETENTION_BATCH_SIZE 10000 Rows deleted per batch
RELIABILITY_HISTORY_RETENTION_MAX_BATCHES 50 Maximum batches per nightly run
ML_OUTPUT_RETENTION_DAYS 365 Days to keep stored ML model outputs, floored at 30
ML_OUTPUT_RETENTION_BATCH_SIZE 5000 Rows deleted per batch
ML_OUTPUT_RETENTION_MAX_BATCHES 50 Maximum batches per nightly run
USER_RISK_RETENTION_DAYS 90 Days to keep resolved user-risk signal history, floored at 30
USER_RISK_RETENTION_BATCH_SIZE 5000 Rows deleted per batch
USER_RISK_RETENTION_MAX_BATCHES 50 Maximum batches per nightly run

Audit-log retention is not set here — it is a per-organization policy an operator saves in the app. See Audit Logs → Retention.

Device history charts, capacity trends, AI performance tools and anomaly detection read the metric_rollups table. It stores each metric at three resolutions: 5-minute, hourly and daily buckets. Each resolution has its own retention window, and each month of data is stored as one partition per resolution. When a resolution’s window passes a whole month, the maintenance job drops that month’s partition for the resolution. Dropping a partition returns its disk space to the operating system immediately. No row-by-row DELETE is involved, and no VACUUM FULL is needed.

Variable Default Minimum Description
METRIC_ROLLUP_5M_RETENTION_DAYS 90 30 Days to keep 5-minute buckets. About 92% of the table, so this window sets most of its size.
METRIC_ROLLUP_HOURLY_RETENTION_DAYS 548 365 Days to keep hourly buckets. Raised to at least the 5-minute window.
METRIC_ROLLUP_DAILY_RETENTION_DAYS 1095 730 Days to keep daily buckets. Raised to at least the hourly window. Past this, the whole month is dropped.
METRIC_ROLLUP_MAINTENANCE_CRON 13 3 * * * — When the daily maintenance job runs (partition creation, retention, compaction).
METRIC_ROLLUP_MAINTENANCE_ENABLED true — Set to false to stop the job. Retention then stops too, and the table grows without limit.
METRIC_ROLLUP_PARTITION_MONTHS_AHEAD 3 1 How many months of partitions to create in advance.
METRIC_ROLLUP_DELETE_BATCH_SIZE 5000 100 Batch size for the one remaining row delete, on metric_rollups_default. That partition only holds rows written for a month that had no partition, and is normally empty.

Values below the minimum are raised to the minimum. Retention runs at whole-month granularity: a month is dropped once all of it is older than the window. With the 90-day default, you keep between 90 and about 120 days of 5-minute data. Disk use for metric_rollups levels off once each window has filled.

Before this change, each month was one partition that held all three resolutions. 5-minute rows were removed by a capped DELETE, which never gave the space back. On upgrade:

  • The migration converts empty pre-created future months to the new per-resolution layout. New data lands in the new layout from the next month onwards.
  • Months that already hold data keep the old layout and keep working. Once a month’s 5-minute data is past METRIC_ROLLUP_5M_RETENTION_DAYS, the maintenance job rewrites it. The job copies only the hourly and daily rows, about 8% of the month, into the new layout, then drops the old table. That frees the month’s whole footprint, including the dead space left by the old deletes.
  • To reclaim space sooner, lower METRIC_ROLLUP_5M_RETENTION_DAYS, down to the minimum of 30, and restart. The next maintenance run compacts every older month.

Each compaction runs in its own transaction and only blocks writes to the month being rewritten; reads continue throughout. The final switch takes a short exclusive lock on metric_rollups, devices and organizations, waits at most 5 seconds for it, and only changes catalog entries. If it cannot get the lock in time it rolls back, and the next nightly run retries it. A failed step fails the metric-rollup-maintenance job with the month named, so it shows up in job monitoring.

Two nightly jobs protect the tamper-evident audit chain described in Audit Logs → Tamper Evidence. The verifier (04:15 UTC) re-checks the chain. It is bounded rather than walking the whole table every night. The anchor job (04:45 UTC) records each organization’s chain head in the append-only audit_chain_anchors table, so a chain rebuilt after rows were deleted is caught. Both jobs are on by default.

Variable Default Description
AUDIT_CHAIN_VERIFY_ENABLED true Set to false to turn nightly verification off. From v0.110.0 this also stops a sweep that is already running, not just the schedule — previously, restarting the API with the flag off could hand the in-flight sweep to the fresh container and re-run it from the beginning
AUDIT_CHAIN_VERIFY_MODE incremental incremental verifies new rows since the last anchor plus one rolling slice of history per night. Set to full to restore the old whole-table walk — expect substantially more database I/O
AUDIT_CHAIN_VERIFY_RESCAN_SLICES 30 How many nightly slices the historical portion of the chain is divided into. At the default, every historical row is re-verified at least once a month
AUDIT_CHAIN_ANCHOR_ENABLED true Set to false to stop scheduling the daily anchor job. Leave it on: the anchor is the only check that catches a chain rebuilt after a DELETE.
AUDIT_ANCHOR_SIGNING_KEY — Signs each anchor with Ed25519. The value is a base64-encoded 32-byte seed. When unset, anchors are still written but unsigned: signature and signing_key_id are NULL. A value that does not decode to exactly 32 bytes makes every anchor write fail and is logged as an error. See Anchor signing below.
AUDIT_ADMIN_DATABASE_URL — postgres:// URL for a breeze_audit_admin login, used only by audit retention. This keeps the delete permission off the API’s request connection. When unset, retention falls back to the legacy shared-credential path and the API logs a SECURITY warning at boot. Set the role’s password first (ALTER ROLE breeze_audit_admin PASSWORD '…'). The full runbook is in apps/api/migrations/2026-06-11-e-audit-retention-revoke-breeze-app.sql.

Up to and including v0.119.0, the shipped compose files did not pass these six variables to the API container, so setting them in .env had no effect. If you set any of them before, they take effect once you upgrade, so check them first.

An unsigned anchor guards against the API’s own database role, which can insert anchors but never change or delete them. It does not guard against someone who can write to the database directly, because that person can also rewrite the anchors. A signed anchor that you keep outside the database closes that gap: nobody can forge one without the seed, and the seed is not stored in the database.

  1. Generate a seed and add it to .env:

    Terminal window
    echo "AUDIT_ANCHOR_SIGNING_KEY=$(openssl rand -base64 32)" >> .env

    Treat it like JWT_SECRET. Keep a copy somewhere other than the server, because a new seed means a new public key.

  2. Restart the API (and the worker, if you run one). At boot the API logs the key id and the public key:

    [AuditChainAnchor] Anchor signing enabled: keyId=anchor-1a2b3c4d5e6f7a8b publicKey(SPKI DER, base64)=MCowBQYDK2VwAyEA…

    Record the public key with whatever verifies your anchors. You can read it again from any later boot log.

  3. Keep the anchors off the server. Each anchor is also written to the API container’s stdout as one JSON line tagged "evt":"audit_chain_anchor". The line contains canonical (the exact signed message), signature (base64) and signingKeyId. Breeze does not ship these lines anywhere by itself. To keep a copy outside the server, forward the API container’s stdout with a log shipper such as Vector, Fluent Bit or promtail.

The in-database check (audit_chain_verify_anchor) compares chain heads only and does not read the signature. The signature is useful only for a copy you keep somewhere else. To check a forwarded line, verify signature over the canonical string with the public key you recorded:

// verify-anchor.mjs: node verify-anchor.mjs '<anchor JSON line>' '<public key, base64>'
import { createPublicKey, verify } from 'node:crypto';
const [line, publicKeyB64] = process.argv.slice(2);
const anchor = JSON.parse(line);
const key = createPublicKey({ key: Buffer.from(publicKeyB64, 'base64'), format: 'der', type: 'spki' });
const ok = verify(null, Buffer.from(anchor.canonical, 'utf8'), key, Buffer.from(anchor.signature, 'base64'));
console.log(ok ? 'valid' : 'INVALID');

Verify the canonical string exactly as it was forwarded. Do not rebuild it from a database row, because the timestamp formatting can differ. A line with "signed":false has no signature. This happens when no key was set, or when the chain head moved while the anchor was being signed. The next day’s anchor covers that case.

A signature proves that the anchor came from a server that held the seed. It does not protect you if the whole host is compromised, because the seed is in the API’s environment. If you change the key, older anchors keep their old signingKeyId, so keep the old public keys to check older anchors.

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.

When the API falls behind — its event loop is slow, or requests queue for a database connection — it can refuse low-priority agent work before that work costs anything, and pace how fast agents reconnect their WebSockets. Refused requests get 503 with a Retry-After, which every agent since v0.64.3 honours; the agent keeps the data where it would on any failed send. Heartbeats, command results, event logs, token rotation and uninstall are never refused.

observe is the default: the API computes everything and counts what it would refuse (breeze_admission_decisions_total{outcome="would_shed"}) but refuses nothing. Switch to enforce once those counters look right for your fleet. off disables it.

Any database pool-acquire timeout or RLS-prologue timeout in the last 30 seconds holds the level at 2 or higher and blocks recovery below it. Only agent WebSocket requests that carry Upgrade: websocket are paced; other requests to the WebSocket path are not counted against the connection budget.

Variable Default Description
BREEZE_ADMISSION_MODE observe off, observe or enforce.
BREEZE_ADMISSION_LAG_MS 100,250,600 Median event-loop lag (ms) that raises the level to 1, 2 and 3.
BREEZE_ADMISSION_DB_WAIT_MS 75,200,600 Time requests wait for a database connection (ms) that raises the level to 1, 2 and 3.
BREEZE_ADMISSION_EMERGENCY_MS 1000 A single stall, or a database wait, this long jumps straight to level 3.
BREEZE_ADMISSION_BULK_MAX_IN_FLIGHT 15 Inventory-type uploads processed at once. Applies in enforce mode only; observe counts but never refuses.
BREEZE_ADMISSION_WS_RATE_PER_SEC 10 Agent WebSocket connections accepted per second at level 0 (halved at 1, a fifth at 2, a twentieth at 3).
BREEZE_ADMISSION_WS_BURST 20 How many WebSocket connections can arrive at once before pacing starts.
BREEZE_ADMISSION_WS_MAX_SETUPS 8 WebSocket connections being set up at the same time.

Level 1 refuses inventory-type uploads (hardware, software, disks, network, process samples, agent logs, UniFi). Level 2 also refuses periodic status uploads (security status, sessions, patches, monitoring results). Level 3 only paces WebSockets harder — if an instance stays at level 3 it needs more capacity, not more shedding. Invalid values fall back to the default with a startup warning; they never stop the API from booting.

The API’s Postgres driver (postgres.js) can leave a pooled connection permanently unable to reconnect if that connection’s socket dies while a write is still buffered. The affected pool slot then retries forever, failing every time with write CONNECT_TIMEOUT — against a database that is completely healthy. Slots are lost one at a time, so the pool decays over hours until most queries fail at connect. Nothing in the API can repair it: only restarting the API process recovers the pool.

The watchdog exists because that failure is very easy to misread as a database outage. When the CONNECT_TIMEOUT rate breaches the threshold, it opens one brand-new connection to the same database, outside the pool. A fresh connection is unaffected by the defect, so the result separates the two causes:

  • the fresh connection succeeds → the database is fine and the pool is broken. Restart the API.
  • the fresh connection also fails → a genuine database, network, TLS, or auth fault. Restarting the API will not help.

The full verdict — with counts, rates and probe timing — is written to the container log. Sentry receives a throttled event carrying the db_pool_health_verdict tag only; Breeze strips message text and structured extras from every outgoing Sentry event, so treat Sentry as the alert channel and the log or /metrics as the detail. The numbers are exported on /metrics/scrape:

Series Meaning
breeze_db_pool_health{verdict="…"} 1 for the current verdict, 0 for the others. Verdicts are below-threshold, pool-degraded, database-unreachable, unknown.
breeze_db_connect_timeouts_total{cause="…"} Connect timeouts by diagnosed cause (connectivity, event-loop-starvation, unknown).
breeze_db_connect_timeout_rate_per_min The rate the watchdog evaluates.
breeze_db_pool_health_last_check_timestamp_seconds When the last evaluation completed. 0 = never.
breeze_db_pool_health_check_failures Evaluations that threw before producing a verdict.
breeze_db_pool_health_probe_close_failures Probe clients whose close failed — each a possible leaked connection.
breeze_db_pool_admission_permits / _in_use / _waiting Request-transaction permits configured, held and waited for. -1 when the process has no request pool.
breeze_db_pool_admission_abandoned / _effective_permits Permits held by transactions abandoned at their connection-setup deadline or after the pool-acquire budget expired (see [db-pool-acquire] and [db-wedged-backend] logs), and the permits actually available (permits − abandoned).
breeze_db_pool_admission_cancelled_waiters_total Requests that gave up waiting for a permit before ever reaching the database.
breeze_db_pool_admission_abandoned_total, breeze_db_pool_admission_abandoned_returned_total{how="…"} Permits abandoned, and returned by rollback (the slow statement finished) or connection-closed (a wedged connection was reclaimed).
breeze_db_pool_acquire_timeouts_total{timer="…"}, breeze_db_prologue_deadline_expiries_total{timer="…"} Pool-acquire and connection-setup timeouts; timer="late" means the API’s event loop was stalled at the deadline.

Alert on breeze_db_pool_health{verdict="pool-degraded"} == 1. There is deliberately no healthy verdict: the quiet one is below-threshold, because the underlying count only sees timeouts that reach an error handler and the watchdog never observes pool slot occupancy. It can tell you something is wrong; it cannot certify that nothing is. For the same reason, all verdict series read 0 before the first evaluation and after a failed one — never infer health from the absence of a bad verdict, and pair any alert with a freshness check on breeze_db_pool_health_last_check_timestamp_seconds.

The watchdog runs by default; you do not need to set any of these.

Variable Default Description
DB_POOL_HEALTH_INTERVAL_MS 60000 How often the watchdog evaluates. Floored at 5000.
DB_POOL_HEALTH_WINDOW_MS 300000 Trailing window the CONNECT_TIMEOUT count is taken over. Floored at 30000.
DB_POOL_HEALTH_MIN_TIMEOUTS 10 Timeouts within the window that trigger the fresh-connection probe. Below this, no probe runs and no connection is opened.
DB_POOL_HEALTH_PROBE_TIMEOUT_MS 5000 Hard bound on the probe. Keep it well under the interval so a slow probe cannot overlap the next tick.
DB_POOL_HEALTH_CAPTURE_THROTTLE_MS 900000 Minimum gap between Sentry captures of the same verdict. A degraded pool stays degraded until restart, so an unthrottled capture would report it every tick. 0 disables the throttle. Console logging is never throttled.
DB_POOL_HEALTH_DISABLED — Set to 1, true, yes, or on to turn the watchdog off. With it off, a poisoned pool decays silently until someone notices the 503s.

Like event-loop lag, these figures are kept off the unauthenticated /health/ready for the same reason — they describe how close the instance is to falling over.

v0.117.0+. The bundled Redis runs with maxmemory-policy noeviction (size set by REDIS_MAXMEMORY, default 256mb): when it fills, writes fail loudly rather than silently evicting queue or session data. Before this watchdog, the first sign of a full Redis was a failed login or a stuck queue. The API now reads Redis memory usage on an interval and logs a warning (and reports to Sentry, throttled) once usage crosses a threshold, so you can raise REDIS_MAXMEMORY ahead of an incident. It only observes; it never changes the Redis eviction policy. If Redis has no maxmemory limit at all, it logs that on every check instead, because no usage ratio can be computed.

It publishes these series on /metrics:

Metric Meaning
breeze_redis_used_memory_bytes Redis used_memory from the last check.
breeze_redis_maxmemory_bytes Redis maxmemory; 0 means unbounded.
breeze_redis_memory_used_ratio used_memory / maxmemory. -1 when Redis is unbounded, or when the last check failed or has not run yet.
breeze_redis_memory_last_check_timestamp_seconds When the last successful check completed. 0 = never.
breeze_redis_memory_check_failures Checks that failed before producing a reading, since process start.
breeze_redis_memory_check_skipped_total Ticks skipped because the previous check was still running.

Pair any alert on the ratio with a freshness check on breeze_redis_memory_last_check_timestamp_seconds. A broken watchdog reports -1, not a stale healthy value.

The watchdog runs by default; you do not need to set any of these.

Variable Default Description
REDIS_MEMORY_MONITOR_INTERVAL_MS 60000 How often Redis memory is checked.
REDIS_MEMORY_WARN_RATIO 0.8 Usage ratio (greater than 0, at most 1) at which the warning fires. Any other value falls back to 0.8.
REDIS_MEMORY_CAPTURE_THROTTLE_MS 900000 Minimum gap between Sentry reports of the same condition. 0 disables the throttle. Console logging is not throttled.
REDIS_MEMORY_MONITOR_DISABLED — Set to true to turn the watchdog off.

/ready and /health/ready answer a real readiness question — are Postgres, Redis and the background consumers this process needs actually up — as distinct from /health, which is liveness only and answers 200 well before that. Point a load balancer or Compose healthcheck that should hold traffic back at /ready. See Health Monitoring for the response shapes and for the worker container’s own variant of the endpoint.

Neither of these needs setting; the defaults suit every deployment we know of.

Variable Default Description
READINESS_CACHE_TTL_MS 5000 How long a readiness verdict is reused before the probes run again, so a busy load balancer cannot turn a healthcheck into a database workload. Accepted range is 0–10000 minus the probe timeout below.
READINESS_PROBE_TIMEOUT_MS 3000 Hard bound on each individual Postgres and Redis probe. Clamped to 100–5000 — a wedged dependency reports not-ready rather than hanging the healthcheck.
Variable Default Description
OFFLINE_DETECTOR_WORKER_CONCURRENCY 5 How many offline-sweep jobs the offline detector processes at once. Clamped to 1–20; anything unparseable falls back to 5. Raise it only if offline detection visibly lags on a very large fleet.

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. In a multi-region deployment use a region-qualified value (production-us, production-eu) — a bare production on every region makes them indistinguishable in Sentry, and server_name is a per-deploy container hash, so nothing else can attribute an error to a region.
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.

Most per-endpoint rate limits (MFA, dashboard usage) are fixed in code and not configurable via environment variables. A few are deployment-tunable: the per-org agent cap, which bounds how much traffic a single tenant’s fleet can generate even with thousands of agents, plus the account lockout and refresh-token limits on the login path.

Variable Default Description
AGENT_ORG_RATE_LIMIT_PER_MIN 600 Floor for the per-organization sliding-window rate limit on agent-authenticated endpoints. The effective ceiling scales with the org’s enrolled device count (see AGENT_ORG_RATE_LIMIT_PER_DEVICE), so this value only applies to small orgs. Returns 429 with a Retry-After derived from the window reset. Not mapped in the stock compose file — to change it, add it to the api service environment: block as well as .env.
AGENT_ORG_RATE_LIMIT_PER_DEVICE 12 Requests per minute allowed per enrolled device when sizing the per-organization ceiling. A steady-state agent uses ~2 req/min, so the default leaves roughly 6x headroom for synchronized bursts, manual scans and command traffic.
AGENT_ORG_RATE_LIMIT_MAX 20000 Hard platform ceiling for the per-organization limit, regardless of device count. Keeps the per-tenant guardrail meaningful for very large fleets.
LOGIN_ACCOUNT_LOCKOUT_MAX 5 Consecutive failed password attempts (keyed per account, not per IP) before the account is locked for the window below. Set to 0 to disable lockout entirely. Not mapped in the stock compose file — add it to the api service environment: block as well as .env.
LOGIN_ACCOUNT_LOCKOUT_WINDOW_SECONDS 900 Lockout duration once LOGIN_ACCOUNT_LOCKOUT_MAX is reached. Not mapped in the stock compose file — add it to the api service environment: block as well as .env.
AUTH_REFRESH_RATE_LIMIT 60 Requests per window allowed on POST /auth/refresh, keyed per refresh-token family (roughly one browser session chain).
AUTH_REFRESH_RATE_WINDOW_SECONDS 60 Window for AUTH_REFRESH_RATE_LIMIT.

v0.118.0+. These limits keep one device, or one organization’s fleet, from using more than its share of the API. The defaults sit well above normal fleet traffic, so you do not need to set any of them. None is mapped in the stock Compose files: to change one, add it to .env and to the api service environment: block (and the worker block if you run the worker split).

Agent WebSocket messages. Each agent connection has a message budget: a burst capacity plus a steady refill rate. Messages over budget are dropped. A connection that keeps sending after this many dropped messages in a row is closed. Command results and update-status messages have their own budgets.

Variable Default Description
AGENT_WS_MESSAGE_BUDGET_CAPACITY 300 Burst capacity, in messages, for general agent WebSocket traffic.
AGENT_WS_MESSAGE_BUDGET_REFILL_PER_SECOND 60 Messages per second added back to the general budget.
AGENT_WS_MESSAGE_BUDGET_CLOSE_THRESHOLD 200 Dropped messages in a row, on any budget, before the connection is closed.
AGENT_WS_COMMAND_RESULT_MESSAGE_BUDGET_CAPACITY 600 Burst capacity for command-result messages.
AGENT_WS_COMMAND_RESULT_MESSAGE_BUDGET_REFILL_PER_SECOND 120 Refill rate for command-result messages.
AGENT_WS_UPDATE_STATUS_MESSAGE_BUDGET_CAPACITY general capacity Burst capacity for update-status messages.
AGENT_WS_UPDATE_STATUS_MESSAGE_BUDGET_REFILL_PER_SECOND general refill rate Refill rate for update-status messages.

Daily ingest budgets. Agent log, event-log and change-log uploads are counted per device and per organization per day. An upload over budget is refused with 429.

Variable Default
AGENT_LOG_MAX_ROWS_PER_DEVICE_PER_DAY 2000000
AGENT_LOG_MAX_BYTES_PER_DEVICE_PER_DAY 524288000 (500 MiB)
AGENT_LOG_MAX_ROWS_PER_ORG_PER_DAY 20000000
AGENT_LOG_MAX_BYTES_PER_ORG_PER_DAY 5368709120 (5 GiB)
AGENT_EVENTLOG_MAX_BYTES_PER_DEVICE_PER_DAY 2147483648 (2 GiB)
AGENT_EVENTLOG_MAX_ROWS_PER_ORG_PER_DAY 2000000
AGENT_EVENTLOG_MAX_BYTES_PER_ORG_PER_DAY 10737418240 (10 GiB)
AGENT_CHANGES_MAX_ROWS_PER_DEVICE_PER_DAY 500000
AGENT_CHANGES_MAX_BYTES_PER_DEVICE_PER_DAY 209715200 (200 MiB)
AGENT_CHANGES_MAX_ROWS_PER_ORG_PER_DAY 5000000
AGENT_CHANGES_MAX_BYTES_PER_ORG_PER_DAY 2147483648 (2 GiB)

Event-log rows per device are already limited by the event-log policy’s hourly rate, so there is no per-device row budget for event logs.

Other limits.

Variable Default Description
HELPER_SCREENSHOT_RATE_LIMIT 12 Screenshot uploads per device from Breeze Helper per window.
HELPER_SCREENSHOT_RATE_WINDOW_SECONDS 60 Window for HELPER_SCREENSHOT_RATE_LIMIT.
SCREENSHOT_MAX_BYTES 1600000 Largest single screenshot stored.
SCREENSHOT_MAX_PER_DEVICE 20 Stored (unexpired) screenshots per device.
SCREENSHOT_MAX_BYTES_PER_DEVICE count × size above Total stored screenshot bytes per device.
LOG_FORWARDING_MAX_JOB_BYTES 1048576 Largest batch of events one log-forwarding job may carry.
LOG_FORWARDING_MAX_PENDING_PER_ORG 2000 Log-forwarding jobs one organization may have queued at once. A slow destination for one organization no longer holds up the others.
LOG_FORWARDING_MAX_WAITING_GLOBAL 10000 Instance-wide ceiling on waiting log-forwarding jobs.
TENANT_TOOL_MAX_IN_FLIGHT_PER_ORG 8 Concurrent calls one organization (or partner, for partner-owned tools) may have in flight across all its connected tool sources.
TURN_CREDENTIAL_MINT_LIMIT_PER_WINDOW 30 TURN credentials one user may request per window. Over the limit, the request returns 429. 0 turns the limit off.
TURN_CREDENTIAL_MINT_WINDOW_SECONDS 600 Window for TURN_CREDENTIAL_MINT_LIMIT_PER_WINDOW.
PUBLIC_DOWNLOAD_KEY_MAX_TTL_MINUTES 1440 (24 h) Longest lifetime of the short-lived download key an installer link hands out.

v0.118.0+. All optional and not mapped in the stock Compose files; add any you set to the api service environment: block as well as .env.

Variable Default Description
BREEZE_LEGACY_ALERTING_SWEEP on Set to false to skip converting and retiring leftover legacy alert rules and service/process watches at boot. The API still counts and logs what is left. See the v0.118.0 upgrade notes.
AGENT_ROLLBACK_DENYLISTED_VERSIONS unset Comma-separated agent versions that an agent rollback may never target. Unset means no restriction.
AGENT_ROLLBACK_MIN_VERSION unset Lowest agent version a rollback may target. An invalid version is ignored. Unset means no restriction.
EMAIL_DOMAINS_ADOPT_EXISTING_ALLOWLIST unset Comma-separated sending domains that may be adopted when they already exist at the email provider without the _breeze-verify.<domain> TXT record. Only needed for a migration. See Custom Sender Addresses.

The four OFFLINE_DETECTOR_REEVAL_* variables were removed in v0.118.0. The job that read them was retired with legacy alerting, so you can delete them from .env.

Variable Default Description
DB_POOL_MAX 30 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).
Variable Default Description
MAX_ACTIVE_REMOTE_SESSIONS_PER_ORG 10 Concurrent remote sessions per org
MAX_ACTIVE_REMOTE_SESSIONS_PER_USER 5 Concurrent remote sessions per user
PATCH_REPORT_STORAGE_PATH ./data/patch-reports Patch compliance report storage
PATCH_REPORT_RETENTION_DAYS 30 Days a generated patch compliance report file is kept after it completes (minimum 1). A daily job then removes the file and marks the report expired. A value that is not a plain integer falls back to the default.
PATCH_REPORT_ORPHAN_MIN_AGE_MS 3600000 (1 hour) How old a <uuid>.csv file in PATCH_REPORT_STORAGE_PATH that no report owns (for example, left behind when an organization was erased and the file could not be removed at the time) must be before the daily job removes it. Never below 10 minutes.

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.

Full explanation of what each one is for: Rollout Modes & Compatibility Switches.

Variable Default Description
EVENT_PERMISSION_EPOCH_MODE compat 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.
REMOTE_DESKTOP_FENCE_REQUIRED true Remote desktop refuses to start (503 agent_upgrade_required, “Remote desktop needs an agent update on this device”) on any agent that has not declared the durable start/terminal fence capability (desktopFenceProtocolVersion=1). Agents v0.114.0 and later declare it. Set false only as a temporary opt-out while older agents finish updating; any other value refuses boot.
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.
AUTH_BROWSER_TERMINAL_PREPARATION_ENABLED false Enables the terminal/native logout-preparation flow described in Cloudflare Access trust. Validated at boot: must be a boolean (true/false, 1/0, yes/no, on/off) when set. It no longer depends on any other flag.
Variable Default Description
ENABLE_REGISTRATION true Allow new user registration
PENDING_ACCOUNT_MEETING_URL — Optional absolute http(s) URL to a scheduling page. When set, a partner whose account is still pending sees it as a secondary link beside the billing call-to-action on the account-inactive screen, so a partner who cannot complete payment is not left at a dead end. Values that are not http/https are ignored.
PENDING_ACCOUNT_MEETING_LABEL Book a call with us Optional link text for the pending-account booking link above.
ENABLE_2FA true Enable two-factor authentication
ENABLE_API_DOCS_UI on outside production, off in production 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 true Surface network-discovered assets alongside agent endpoints in the Devices list view (on by default since v0.111). Set false at web image build time to return to the agent-only list.
BREEZE_WORKSPACE_ENABLED false in the production compose files (true in the dev override) Load the built-in AI Workspace extension. Off by default — leave it unset unless you want Workspace. Requires a PostgreSQL image that provides the pgvector extension (see the aside below).
BREEZE_AI_AGENTS_ENABLED false Surface AI Agents (Settings → AI Agents). Off by default — no agent exists, runs, or appears in the UI until this is true. Enabling the flag does not by itself let an agent change anything: every agent starts in Off, and moving one to Act is a separate, MFA-confirmed opt-in per agent. Must also be mapped in the api service’s environment: block, not just set in .env.
BREEZE_AI_AGENTS_POLICY_DECIDE_ENABLED false Sub-flag of BREEZE_AI_AGENTS_ENABLED for the policy-decide capability, which is not yet generally available — leave it off. When enabled it lets an agent authorise a supervised-scope action on its own policy decision instead of sending it to a human approver, and it is what allows an operation to be promoted to that status from the Graduation panel. Automatic demotion is not gated by this flag and is always on. An operation only ever goes live on the organization’s own agent — a partner-wide list is a ceiling, not a grant, so grant each organization explicitly before enabling this. See AI Agents → Graduation. An unrecognised value stops the API at boot rather than silently reading as off.
BREEZE_AI_AGENTS_SWEEP_ACT_ENABLED false Sub-flag of BREEZE_AI_AGENTS_ENABLED (also requires BREEZE_AI_AGENTS_POLICY_DECIDE_ENABLED) that lets a sweep schedule opt into Act mode, so intents an AI sweep raises can reach the same policy-decide path as other agent actions. Off by default.
TOOL_SOURCES_ENABLED false Enables external tool sources for AI Agents. Off by default; even when enabled, any tier-3 (destructive) external tool call still goes through the normal action-intent approval flow — this flag does not bypass approval.
TOOL_SOURCES_ALLOW_PRIVATE_EGRESS false Self-hosted only — refused when IS_HOSTED=true. Allows a tool source URL to resolve to a private, link-local, or loopback address. Leave off unless you specifically need a tool source on your internal network.
CALLER_VERIFICATION_ENABLED false v0.116.0+. Backend-only in this release — there is no reachable UI yet. Must be the exact literal true to enable; every /caller-verification route answers 404 until then, and any other truthy-looking value (1, yes, on) is refused at boot rather than treated as enabled. Leave unset unless you are specifically testing this feature ahead of its UI.
DEVICE_COMMAND_QUEUE_TTL_HOURS 168 (7 days) How long a queued standard action may wait for its device before it expires undelivered. Covers scripts, patch installs, patch scans, software installs, and automation actions.
DEVICE_COMMAND_QUEUE_SHORT_TTL_HOURS 24 Deadline for self-superseding queued work — inventory refreshes, log-level changes, agent auto-update and rollback, browser-policy application, and the audit, security, reliability, hardware and encryption-key collections. The next run of any of these replaces the last, so a week-old copy would be stale by the time it landed.
DEVICE_COMMAND_QUEUE_POWER_STATE_TTL_HOURS 24 Deadline for queued power-state work: reboot, shutdown, and scheduled restart.

Event dispatch settings select how a subscriber receives events. They do not enable or disable the feature behind that subscriber. For example, the ticket-helpdesk and anomaly agent lanes still run in-process with the default off mode. See AI Agents for lane requirements and durable trigger guidance.

Variable Default Description
EVENT_DISPATCH_MODE off Accepts off, shadow, or enforce. off uses in-process delivery only. shadow also mirrors routing plans into delivery receipts for observability but executes nothing through the queue. enforce sends each named subscriber through the BullMQ queue only, skips its in-process delivery, and leaves unnamed subscribers in-process.
EVENT_DISPATCH_QUEUE_SUBSCRIBERS — Comma-separated subscriber IDs used only when the mode is enforce: ai-agent-alert-verdict, ai-agent-anomaly, ai-agent-ticket-helpdesk, automation-worker, dns-threat-alerts, notification-dispatcher, policy-alert-bridge, webhook-delivery. An empty list is accepted but leaves everything in-process and produces a boot warning. The variable is ignored in other modes.

Both variables are validated when the API starts. An invalid mode or an unknown subscriber ID stops the API from starting and identifies the invalid value. As with other API settings, map each variable in the api service’s environment: block of your compose file as well as setting it in .env.

Variable Default Description
MCP_SSE_RATE_LIMIT_PER_MINUTE 30 SSE connection rate limit per API key
MCP_MESSAGE_RATE_LIMIT_PER_MINUTE 120 Message rate limit per API key
MCP_TOOLS_LIST_PAGE_SIZE 0 0/unset = single page; set to e.g. 50 once every connected client honours nextCursor. Cursor offsets are best-effort if the visible tool list changes or tenant-tool resolution fails.
MCP_MAX_SSE_SESSIONS_PER_KEY 5 Max concurrent SSE sessions per API key
MCP_REQUIRE_EXECUTE_ADMIN true In production, Tier 3+ tools require the ai:execute_admin scope. Set to false to fall back to ai:execute. No effect outside production.
MCP_EXECUTE_TOOL_ALLOWLIST — Comma-separated allowed Tier 3 tools (empty = deny all)
MCP_UNATTENDED_TIER3_PRINCIPALS — Operator opt-in, empty = nobody. By default every Tier 3 tool/action (and collect_evidence) is denied over MCP with MCP_APPROVAL_REQUIRED because MCP has no interactive approval step. Comma-separated api_key:<api key id> / oauth_client_user:<client_id>/<user id> / partner_sp:<partner service principal id> entries (OAuth binds client AND user; a shared client_id alone never qualifies; a partner_sp entry names the principal, so key rotation keeps it, and like an OAuth entry it applies in every org the principal reaches) lift only that deny, only for those principals (trusted unattended agents). ai:execute, ai:execute_admin (production), MCP_EXECUTE_TOOL_ALLOWLIST (production), RBAC, rate limits, the Tier 3 execution ledger and the audit log still apply, and collect_evidence is gated as Tier 3. Tier 4, tenant (BYO MCP) tools and the onboarding bootstrap tools stay approval-only. At startup the API logs a WARN listing the configured principals, and a separate WARN for any entry that is not well formed (it can never match). Each bypassed call’s audit row carries approvalBypass: "unattended_principal" and approvalBypassPrincipal (the matched entry).
Variable Default Description
CLOUDFLARE_API_TOKEN — Cloudflare API token with Client Certificates permission
CLOUDFLARE_ZONE_ID — Cloudflare zone ID for your domain
Variable Default Description
CF_ACCESS_TRUST_ENABLED false Set true to short-circuit POST /auth/login when a valid Cf-Access-Jwt-Assertion header is present. Off by default. See Cloudflare Access trust.
CF_ACCESS_TEAM_DOMAIN — Required when trust is enabled. Bare hostname of your Cloudflare team domain, e.g. example.cloudflareaccess.com. No scheme.
CF_ACCESS_AUD — Required when trust is enabled. AUD tag for the Cloudflare Access application protecting Breeze.
CF_ACCESS_TRUSTS_MFA false Treat a valid CF Access JWT as MFA-satisfied for the minted Breeze session. Only enable if your CF Access policy actually requires step-up MFA.
Variable Default Description
AGENT_BACKUP_SERVER_URL — Instance-level backup control-plane URL, pushed to every agent through its config, enrollment response, and heartbeat reply. Lets you point the enterprise backup subsystem (Hyper-V, MSSQL, system image) at a separate backup control plane instead of the main API. Must be https:// in production; http:// is accepted only for localhost/127.0.0.1. Leave unset to keep backups pointed at the main API.
RECOVERY_MEDIA_WORK_DIR /data/recovery-work (Compose) Scratch directory for building bare-metal recovery bundles and ISOs (a 45–65 MB helper binary plus a tarball or ISO per build). The api and worker containers mount /tmp as a 64 MB tmpfs, so building under the OS temp directory fails with ENOSPC; the shipped Compose files default this to a directory on the persistent api_data volume. Override only to move it elsewhere on durable storage, and map it in the api service environment: block.
RECOVERY_SIGNING_PRIVATE_KEY — minisign secret key (the key file contents; create it without a password, minisign -G -W) used to sign recovery bundles and their helper binary. From v0.118.0 a bundle built without signing configured refuses to run, because its launch script has no key to verify the helper with. Set it together with RECOVERY_SIGNING_PUBLIC_KEY. See Recovery bundle signing.
RECOVERY_SIGNING_PUBLIC_KEY — The matching minisign public key. It is written into each bundle when the bundle is built.
RECOVERY_SIGNING_KEY_ID derived from the public key Optional identifier for the signing key.
RECOVERY_SIGNING_KEYS_JSON — Optional JSON array of additional published verification keys (keyId, publicKey), for rotating keys.
RECOVERY_MINISIGN_BIN minisign Path to the minisign binary the API runs to sign bundles. It must be available to the API process.
DEV_PUSH_WORK_DIR /data/dev-push (derived from PATCH_REPORT_STORAGE_PATH) Staging directory for dev-push agent binary uploads (non-production, or when DEV_PUSH_ENABLED=true). Never the container’s small /tmp tmpfs. Uploads larger than the free space here are rejected with HTTP 507 naming this directory.
BACKUP_GC_GRACE_MS 172800000 (48 h) How long an expired snapshot’s storage objects are kept before the retention sweep deletes them. Lab/test knob only. In production the API enforces a floor of one hour and logs when a lower value is ignored. Leave unset.
BACKUP_GC_ORPHAN_MANIFEST_MAX_AGE_MS 777600000 (9 d) v0.113.0+, lab/test only. How long an upload that never finished, or whose record could not be reconciled, sits idle before the storage sweep reclaims its prefix. Production floor: 7 days. Re-read on every sweep, no restart needed.
BACKUP_GC_MANIFESTLESS_PREFIX_MAX_AGE_MS 777600000 (9 d) v0.113.0+, lab/test only. How long the data of an upload that never published a restore point sits idle before the storage sweep reclaims it. The window covers the seven days an interrupted backup can still resume. Production floor: 7 days.
BACKUP_BASE_LEASE_MS 604800000 (7 d) v0.113.0+, lab/test only. How long a base restore point another backup is building on stays protected from reclamation, and the deadline by which that backup must publish. The backup helper stops publishing one hour before the deadline, so a value of one hour or less fails every backup. Production floor: 2 hours.
BACKUP_RESTORE_PIN_LINGER_MS 604800000 (7 d) v0.113.0+, lab/test only. How long a restore point stays pinned after a restore or verification referenced it. Production floor: 1 hour.
BACKUP_PUBLISH_MARGIN_MS 3600000 (1 h) v0.113.0+, lab/test only. Safety margin around a restore point’s publish time before its objects are considered for reclamation. Production floor: 5 minutes.
Variable Default Description
C2C_M365_CLIENT_ID — 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 power the native inbound address — the partner@yourdomain address shown on the Inbound email card, which customers mail directly (or which you forward an existing support mailbox to). This is the self-hosted equivalent of what we configure once, centrally, on hosted Breeze. It is independent of the Microsoft 365 mailbox path below: you can run either, both, or neither.

Variable Default Description
TICKETS_INBOUND_DOMAIN — The domain inbound ticket addresses live on, e.g. tickets.yourmsp.com. Each partner’s address is <local part>@<this domain>. Until this is set, the Inbound email card reports that no platform inbound domain is configured (naming this variable and linking back here on self-hosted instances) and no address can be generated.
MAILGUN_INBOUND_SIGNING_KEY — Mailgun’s HTTP webhook signing key, used to HMAC-verify deliveries. The inbound webhook is unauthenticated by design — this signature is its only security — so when the key is unset every delivery is rejected with 401.
INBOUND_QUEUE_MAX_PER_SEC 20 v0.116.0+. Global ceiling on how many inbound email-to-ticket jobs are processed per second across all senders and partners — flood backpressure for the inbound queue. Jobs over the ceiling are delayed, never dropped. Raise it for a high-volume tenant, lower it to slow down a burst; it bounds the rate of ticket creation, not the total.

Setting the variables is only half of it — mail has to reach Breeze:

  1. Add tickets.yourmsp.com to Mailgun as a receiving domain and publish the MX records it gives you.
  2. Create a Mailgun Route matching that domain (catch_all(), or match_recipient(".*@tickets.yourmsp.com")) with the action forward("https://<PUBLIC_URL>/api/v1/webhooks/tickets/email-inbound").
  3. Copy the domain’s HTTP webhook signing key into MAILGUN_INBOUND_SIGNING_KEY.
  4. Set TICKETS_INBOUND_DOMAIN, restart the API, then set the local part and enable email-to-ticket under Settings → Ticketing → Inbound email.

Two failure modes are silent from the sender’s side, so check these first if mail vanishes:

  • Every delivery returns 401 — MAILGUN_INBOUND_SIGNING_KEY is unset or wrong. Mailgun treats this as permanent and will not retry.
  • Deliveries return 429 — the webhook’s per-IP rate limiter fails closed, so this is also what you get when Redis is unreachable. Confirm TRUSTED_PROXY_CIDRS matches your reverse proxy, or the limiter keys every request to the proxy’s own IP.

Breeze trusts the Authentication-Results header only when Mailgun itself stamped it, and requires DMARC pass to mark a sender verified — SPF and DKIM alone are not sufficient.

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, and an alternative to the Mailgun inbound path above — it needs no MX records or DNS changes.

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, and its Application Access Policy PowerShell snippet is filled in with this id at runtime (it is the app’s public client id, not a secret).
TICKET_MAILBOX_M365_CLIENT_SECRET — Client secret for the Breeze Ticketing Azure app.
Section titled “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=true and 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.
M365_CUSTOMER_GRAPH_READ_VAULT_REF Yes akv://<vault-host>/m365-customer-graph-read/<32-lowercase-hex-version> Opaque Key Vault reference to the app’s certificate material. The trailing version segment must match M365_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.

These are read by the API. The master switch defaults off — see the Microsoft 365 tenant sync release note on the release notes page for the flag-on rollout order.

Variable Required Format / example Meaning
M365_TENANT_SYNC_ENABLED — (the switch) true / false (default false) Master switch for the scheduled tenant snapshot (users, sign-in activity, Intune devices, Conditional Access, licenses, Secure Score). Gates every entry point: the ticker, post-consent seeding, the on-demand sync route, and the disconnect hook. Off means no sync work of any kind runs.
M365_SYNC_CONCURRENCY No Positive integer (default 4) Sync jobs processed concurrently per API instance. The Graph fetch holds no database connection, so this bounds persist work, not fetch work.
M365_SYNC_MAX_BACKLOG No Positive integer (default 500) Queue depth (waiting + prioritized + delayed + active) above which the ticker skips a tick. Due rows keep their past due time and are picked up next tick.
M365_SYNC_TICK_BATCH No Positive integer (default 200) Rows claimed per 60-second tick. This is the capacity dial: target at most 50 % utilisation of batch × 1440 runs per day.

Executor-side tenant-sync tuning (v0.113.0+)

Section titled “Executor-side tenant-sync tuning (v0.113.0+)”

These are read by the m365-graph-read-executor sidecar, not the API. They tune the Microsoft 365 tenant-sync feature that ships in v0.113.0. All are optional; the executor refuses to start on a malformed value rather than guessing.

Variable Default Format Meaning
M365_SYNC_CONTINUATION_KEY — (ephemeral per process) Exactly 32 bytes, base64-encoded (44 characters) Secret used to seal sync continuation tokens so a long sign-in-log sync can resume across executor restarts or replicas. When unset, the executor mints a key at boot: continuations then die with the process and the sync restarts that domain from page 1 — a self-healing default, so an existing executor keeps working without it. Set it when you run more than one executor replica.
M365_SYNC_MAX_IN_FLIGHT 4 Integer 1–64 Maximum concurrent tenant-sync operations the executor will run.
M365_MAX_IN_FLIGHT 32 Integer 1–1024 Overall in-flight Graph request ceiling for the executor. Must be greater than or equal to M365_SYNC_MAX_IN_FLIGHT, or the executor refuses to start.
M365_SIGNIN_ACTIVITY_RPM 4 Positive integer Token bucket for sign-in-activity Graph requests. Microsoft’s limit is 10 per minute per application across all tenants, so divide this across every region and replica that shares one app registration.
M365_SIGNIN_PAGES_PER_CALL 5 Positive integer Pages fetched per sign-in call before returning an opaque continuation.
M365_SYNC_MAX_ITEMS_USERS 25000 Positive integer Per-pull user cap. A tenant over the cap syncs as partial and never has vanished rows marked stale; raising the cap is the remedy.
M365_SYNC_MAX_ITEMS_DEVICES 25000 Positive integer Same, for Intune managed devices.
M365_SYNC_MAX_ITEMS_CA 500 Positive integer Same, for Conditional Access policies.
M365_SYNC_MAX_ITEMS_SKUS 200 Positive integer Same, for subscribed SKUs.

Customer Microsoft 365 Graph-actions (write) executor (optional)

Section titled “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, 1 / 0, yes / no, on / off (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. From v0.106.0 a value outside the list above stops the API from starting (it was previously read as “off”, silently leaving the write tools dark), and turning it on additionally requires APP_ENCRYPTION_KEY_ID.
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.
M365_CUSTOMER_GRAPH_ACTIONS_VAULT_REF Yes akv://<vault-host>/m365-customer-graph-actions/<32-lowercase-hex-version> Opaque Key Vault reference to the app’s certificate material. The trailing version segment must match M365_CUSTOMER_GRAPH_ACTIONS_CREDENTIAL_VERSION.
M365_GRAPH_ACTIONS_EXECUTOR_URL Yes Origin-only HTTPS, e.g. https://m365-actions.internal (no path, query, fragment, or userinfo) Private base URL of the Graph-actions executor sidecar. Must be HTTPS; anything other than a bare origin is rejected at boot.
M365_GRAPH_ACTIONS_EXECUTOR_AUDIENCE Yes Literal m365-graph-actions-executor JWT audience for the API→executor internal auth token. Must equal that exact string.
M365_GRAPH_ACTIONS_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_ACTIONS_EXECUTOR_SIGNING_PRIVATE_JWK_FILE Yes 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.
Variable Default Description
BREEZE_VERSION latest Breeze release tag for Docker images (e.g. 0.50.0). Also used by GitHub redirect mode for agent binary downloads.
DOCKER_PLATFORM linux/amd64 Container platform. GHCR images are amd64 only. On Apple Silicon, use docker-compose.override.yml.local-build to build native arm64 images instead.
REDIS_MAXMEMORY 256mb Maximum memory Redis is allowed to use. Redis runs with noeviction policy so BullMQ jobs are never silently dropped — which also means a Redis that reaches this ceiling starts rejecting writes and stays full across a restart, because the AOF replays the same over-cap dataset. Raise this value before you get there; see Redis memory growing.
Variable Default Description
BILLING_SERVICE_URL — URL of the billing service for AI credit checks and deductions. When unset, AI usage is unlimited.
BILLING_SERVICE_API_KEY — API key for authenticating to the billing service
STRIPE_SESSION_REVOCATION_MODE enforce v0.113.0+. How strictly Breeze cancels a customer’s open Stripe Checkout payment link when the invoice it belongs to is reset, paid manually, voided, or the Stripe key is replaced. enforce (default) refuses the transition with 503 STRIPE_REVOCATION_PENDING if it cannot prove every open Checkout session is dead; the revocation intent is kept and retried by a 60-second sweep, so the refusal is short-lived. observe still records the intent and calls Stripe, but a failure never blocks the transition — an incident de-escalation lever, not a normal setting. Any other value is treated as enforce with a warning. Roll back by flipping to observe, never by reverting the migration.
Variable Default Description
ANTHROPIC_API_KEY — The platform Anthropic API key (hosted: Breeze’s key; self-hosted: yours). Partners can add their own key under AI Providers & Models.
ANTHROPIC_BASE_URL — 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-5-5 Override the default model id for a raw vLLM backend. Leave unset with a LiteLLM gateway that aliases claude-sonnet-5-5 → your model (releases before the Sonnet 5.5 default used claude-sonnet-4-6; update the alias when you upgrade). On a self-hosted deployment it is also the default model new partners start with; if the registry does not list it, Breeze adds it once at the documented conservative rate (see below).
AI_PLATFORM_INFERENCE_GEO — Optional. Inference geography sent to Claude for platform-key traffic, where the model and transport support it. Allowed values: us or global. Unset = the provider default. eu is not accepted by the platform key: setting it (or any other value) makes every platform model unavailable (residency_unavailable) rather than sending traffic elsewhere, and the API logs a warning at startup. EU residency is not available on the platform key.
AI_GATEWAY_HEADERS_TIMEOUT_SECONDS — Optional. How long the AI model gateway waits for a bring-your-own OpenAI-compatible endpoint to start responding, in seconds. Defaults to 120 on self-hosted and 30 on hosted; clamped to 5–600. Raise it for slow or CPU-only local model servers with long prompt prefill.
AI_TOOL_SEARCH auto AI chat tool loading. auto: on the Anthropic API, chat sends a small core tool set and loads the rest on demand through tool search; on any other ANTHROPIC_BASE_URL or BYO provider it sends every tool, because many gateways reject tool-search blocks. on: also use tool search on a custom base URL (only if your gateway forwards tool_reference blocks). off: always send every tool. Helper, scripts and AI agents always use a fixed tool set.
BREEZE_AI_SCRIPT_AUTHORING_ENABLED true v0.113.0+. Lets the AI assistant and background agents write a script as a reviewed proposal (AI Script Authoring). Set false to hide the propose_script / get_script_proposal tools and the /api/v1/ai/script-proposals routes entirely. Accepts true/false, 1/0, yes/no, on/off; any other value stops the API from starting. The shipped Compose file defaults it to true as well. This flag does not control the unattended lane, which stays off until a partner raises its ceiling and an organization opts in.
BREEZE_AI_ARTIFACT_CAPTURE_ENABLED false v0.117.0+. Store oversized AI tool results (redacted) in your object storage so the assistant can page back through them with read_artifact, instead of receiving a truncated result. Off by default because it writes blobs your organizations can download. Setting it true requires an artifact bucket and keys: ARTIFACT_S3_BUCKET_US / ARTIFACT_S3_BUCKET_EU (matching your region) or S3_BUCKET, plus ARTIFACT_S3_ACCESS_KEY / ARTIFACT_S3_SECRET_KEY or S3_ACCESS_KEY / S3_SECRET_KEY. Without them, or with a value that is not a boolean, the API refuses to start.

AI Operator hands a long-running task to an agent that keeps working after the browser is closed. It is gated separately from the rest of the AI-agent subsystem, and BREEZE_AI_AGENTS_ENABLED still has to be on above it.

Variable Default Description
AI_OPERATOR_TASKS_ENABLED false Admit AI Operator tasks and their continuation runs. With this off, the Delegate to Operator action does not appear and no task can be created.
AI_OPERATOR_RECIPE_SERVICE_RECOVERY_ENABLED false Allow the one shipped recipe, service recovery. Task admission needs this as well as the flag above.

With both flags off the whole subsystem is inert. Its two background workers — the task coordinator (15-second tick) and the task-event publisher (5-second tick) — are still registered so that a task already in flight when you turn the feature off can finish cleanly, but with no tasks being admitted neither has anything to do. No new queue is added.

Self-hosted: alternative Anthropic-compatible backends

Section titled “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=<backend bearer token>
# 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-5-5 to your backend model, so no ANTHROPIC_MODEL override is needed. For a recognised Claude model id, Breeze sends adaptive thinking and an effort level (medium), so the gateway must accept or drop those parameters (LiteLLM: drop_params: true). If it can do neither, set ANTHROPIC_MODEL to your backend’s own model id instead; Breeze then sends thinking disabled, as before. For an OpenAI-dialect-only endpoint, use MCP_LLM_PROVIDER=openai-compatible with the MCP_LLM_* variables instead. At boot Breeze creates a read-only, env-managed connection per partner with one priced, auto-verified model that supports tools (see Bring Your Own LLM Key). Hosted deployments refuse MCP_LLM_PROVIDER=openai-compatible.

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 uses the platform model registry, never the SDK’s own cost figure. When ANTHROPIC_MODEL names a model the registry doesn’t list, Breeze adds it once (when your partners move to the registry) and prices it at conservative Opus-tier rates ($5/$25 per MTok) — likely an over-estimate for a cheap local model, not $0. For accurate accounting, set that model’s prices on Admin → AI models, or use the openai-compatible path’s MCP_LLM_PRICE_INPUT_PER_M_USD / MCP_LLM_PRICE_OUTPUT_PER_M_USD variables.

The warranty worker looks each physical device’s serial number up against its manufacturer so the device page and warranty-expiry alerts carry real coverage dates. Every vendor is off until configured; an unconfigured vendor leaves its devices at status unknown. Apple coverage is reported by the macOS agent and needs no configuration.

Variable Default Description
DELL_CLIENT_ID — OAuth client ID from Dell’s TechDirect Warranty API
DELL_CLIENT_SECRET — Matching Dell OAuth client secret. Both are required for Dell lookups.
LENOVO_API_KEY — ClientID for Lenovo’s official Warranty API (supportapi.lenovo.com), issued through a Lenovo partner manager
LENOVO_WARRANTY_ENABLED — true enables credential-free Lenovo lookups through the JSON endpoint behind Lenovo’s public warranty-lookup page. It is not documented by Lenovo and may stop working without notice, so it is opt-in.
Variable Default Description
CLIENT_AI_ENTRA_CLIENT_ID — Entra application (client) ID for the multi-tenant Breeze AI for Office add-in app registration. The entire /api/v1/client-ai/admin/* admin surface (tenant mapping, org policy, usage, templates) answers 404 (“Breeze AI for Office is not enabled”) until this is set. Leave unset if you don’t use the Office add-ins’ AI features.
Variable Default Description
QBO_CLIENT_ID — Intuit OAuth client ID for the QuickBooks Online integration
QBO_CLIENT_SECRET — Intuit OAuth client secret
QBO_REDIRECT_URI — OAuth callback URL registered with Intuit (e.g. https://your-domain/api/v1/accounting/quickbooks/callback)
QBO_ENVIRONMENT — sandbox or production
QBO_WEBHOOK_VERIFIER_TOKEN — Intuit app verifier token for POST /api/v1/webhooks/quickbooks. Optional. Without it the webhook endpoint answers 503 on every delivery (so Intuit keeps retrying) and payments arrive on the 15-minute reconcile sweep instead — the sweep is the guaranteed path either way, the webhook only makes it faster. Intuit allows one webhook endpoint per app, so a multi-region deployment can only point it at one region.
Variable Default Description
XERO_CLIENT_ID — Client ID of your Xero Web app (developer.xero.com → My Apps). The Xero card only appears when this is set.
XERO_CLIENT_SECRET — That app’s client secret
XERO_REDIRECT_URI — OAuth callback registered on the app, e.g. https://your-domain/api/v1/accounting/xero/callback
XERO_WEBHOOK_KEY — The app’s webhook signing key. In the Xero app’s Webhooks page, subscribe to Invoices with delivery URL https://your-domain/api/v1/webhooks/xero, then send “Intent to receive”. Without it, payments recorded in Xero still reach Breeze through the 15-minute sweep, just more slowly.
XERO_DAILY_CALL_LIMIT 1000 Per-organisation daily API budget for your Xero tier (Starter 1000; Core, Plus and Advanced 5000). Background sweeps pause below 20% of it; invoice and payment pushes keep priority.
Variable Default Description
NVD_API_KEY — 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.

Partner trust probation (hosted service only)

Section titled “Partner trust probation (hosted service only)”
Variable Default Description
PARTNER_TRUST_MODE off when IS_HOSTED is not true; otherwise shadow off disables the feature entirely. shadow classifies new self-serve partners and records what it would have done without denying anything. enforce places new partners in probation until a card payment settles and ages, or an admin approves them from the trust queue.
IP_CLASSIFY_PROVIDER — IP-reputation provider used to classify a signup’s origin. Unset means signups classify as unknown and promotion out of probation is manual only.
IP_CLASSIFY_API_KEY — API key for that provider. Setting a provider without a key logs a warning and falls back to the offline classifier.
TRUST_ACTION_TOKEN_SECRET falls back to JWT_SECRET Signs the one-time action links in trust-review emails.
PARTNER_MEETING_URL — Booking link offered to a partner under review so they can talk to a human. Omitted from the notice when unset.