Environment variables
Every variable the API and UI read, with validation rules and failure behaviour.
Configuration is validated at startup. The API refuses to start rather than running in an ambiguous state.
API — required
| Variable | Rule |
|---|---|
API_ORIGIN | Absolute URL of the API itself. |
UI_ORIGIN | Absolute URL of the UI. Also the only permitted CORS origin. |
BETTER_AUTH_SECRET | At least 32 characters. |
SCHEDULER_SECRET | At least 32 characters. Guards /internal/scheduler/tick. |
ENCRYPTION_KEY | Base64 that decodes to exactly 32 bytes. |
Generate the encryption key with:
openssl rand -base64 32API — client address behind a proxy
| Variable | Default | Rule |
|---|---|---|
AUTH_TRUSTED_PROXIES | — | Comma-separated IP addresses or CIDR ranges. Node entrypoint. |
Authentication rate limits — sign-in, password reset, verification resend — are
keyed on the client address. The workerd deployment reads Cloudflare's
cf-connecting-ip, which the platform writes before the Worker runs, so it
needs nothing here.
A Node server has only X-Forwarded-For, and a visitor can write that header.
Naming the proxies in front makes Better Auth walk the chain from the right and
stop at the first hop that is not one of them, which is the last address a
trusted proxy actually observed. Set it to your load balancer or ingress
ranges:
AUTH_TRUSTED_PROXIES=10.0.0.0/8,2400:cb00::/32The Node entrypoint refuses to start with NODE_ENV=production and no value,
because otherwise every rate-limit key is chosen by the caller being limited.
Local development leaves it unset.
API — inference
| Variable | Default | Rule |
|---|---|---|
LLM_GATEWAY_API_KEY | — | Required unless DEMO_MODE=true. Never required for the public model directory. |
LLM_GATEWAY_DEFAULT_MODEL | openai/gpt-5-mini | Non-empty string. |
DEMO_MODE | false | true or false. Rejected outright in production. |
NODE_ENV | development | development, production, or test. |
Startup fails when:
NODE_ENV=productionandDEMO_MODE=true;LLM_GATEWAY_API_KEYis absent andDEMO_MODEis nottrue;ENCRYPTION_KEYdoes not decode to 32 bytes; or- the Node entrypoint runs with
NODE_ENV=productionand noAUTH_TRUSTED_PROXIES.
API — Stripe billing
Production requires the Stripe secret, webhook secret, and all three Price IDs. In local development, leave all five unset to disable billing safely, or set all five to Stripe sandbox values. Partial configuration always fails startup.
| Variable | Default | Rule |
|---|---|---|
STRIPE_SECRET_KEY | — | sk_test_… locally or sk_live_… in production; server-side only. |
STRIPE_WEBHOOK_SECRET | — | whsec_… for this environment's /webhooks/stripe endpoint. |
STRIPE_STARTER_MONTHLY_PRICE_ID | — | price_… for the active USD $10 monthly recurring Starter price. |
STRIPE_PRO_MONTHLY_PRICE_ID | — | price_… for the active USD $39 monthly recurring Pro price. |
STRIPE_SCALE_MONTHLY_PRICE_ID | — | price_… for the active USD $99 monthly recurring Scale price. |
STRIPE_CHECKOUT_ENABLED | false | Set true only after launch approval. false blocks only new Checkout sessions. |
STRIPE_AUTOMATIC_TAX_ENABLED | false | Independently enables Automatic Tax for new Checkout Sessions after tax validation. |
The two switches are independent and fail closed. Enabling Checkout does not silently enable Automatic Tax. Keep the tax switch off until registrations, the Product tax code, tax behavior, and a sandbox invoice have been reviewed.
COMMERCIAL_ACQUISITION_ENABLED is a third default-off launch switch shared by
the API and UI. The API requires it before creating a new Checkout Session, and
the UI uses it to expose pricing, Business intake, and acquisition entry points.
Set the same value on both projects; opening the UI alone never authorizes the
API to charge.
BILLING_DEMO_MODE=true enables a deterministic local Checkout and portal
only when DEMO_MODE=true and no Stripe credentials are present. It uses the
real authenticated billing routes, projector, and entitlements, but never
collects payment data. Production rejects it, and the worker deployment does
not expose the variable. Playwright enables it automatically.
The Stripe CLI prints its own temporary whsec_… value when forwarding local
events. It is not the same secret as a Dashboard webhook endpoint.
Empty is the same as unset
An empty assignment such as LLM_GATEWAY_API_KEY= is treated as absent, so a
checked-in .env.example placeholder never satisfies a requirement by
accident.
API — authentication email
Email verification and password reset share one authentication-email provider; password reset requires no new environment variable.
| Variable | Default | Rule |
|---|---|---|
AUTH_EMAIL_CAPTURE_ENABLED | false | Test/local capture only. Must be false in production. |
AUTH_EMAIL_FROM | — | Required Resend sender mailbox in production; use a verified sending domain. |
AUTH_EMAIL_REPLY_TO | — | Required single monitored mailbox in production. |
RESEND_API_KEY | — | Required authentication-email sending secret in production; keep it in runtime storage. |
EMAIL_REPLY_TO | — | Optional shared Reply-To. Fills AUTH_EMAIL_REPLY_TO and LIFECYCLE_EMAIL_REPLY_TO when either is unset; one bare address. |
EMAIL_REPLY_TO is a convenience for running one monitored mailbox: when it
is set, a missing AUTH_EMAIL_REPLY_TO or LIFECYCLE_EMAIL_REPLY_TO takes its
value, and a channel-specific value always wins. It must be a single bare
address (no display name); startup fails otherwise. It never enables a sender
on its own and never changes the local capture decision.
Local development and automated tests may use capture only when
AUTH_EMAIL_CAPTURE_ENABLED=true, NODE_ENV is not production, and all
three provider values are empty. Production startup fails unless capture is
disabled and AUTH_EMAIL_FROM, AUTH_EMAIL_REPLY_TO, and RESEND_API_KEY are
all present. Authentication email remains active when lifecycle email is
disabled. MANAGED_MCP_RESEND_ENABLED remains unrelated: it controls customer
Resend MCP connections, not email verification and password reset.
API — connection providers
Every provider is optional. GitHub uses a registered OAuth client, while PostHog recommends a public Client ID Metadata Document (CIMD) plus PKCE. A provider without its required configuration is listed in the dashboard with its connect action disabled.
| Variable | Default | Notes |
|---|---|---|
GITHUB_OAUTH_CLIENT_ID | — | Enables the GitHub connection. |
GITHUB_OAUTH_CLIENT_SECRET | — | GitHub OAuth apps do not support PKCE. |
POSTHOG_OAUTH_CLIENT_ID | — | Public CIMD URL; enables the PostHog connection. |
POSTHOG_OAUTH_CLIENT_SECRET | — | Legacy confidential clients only; omit when using CIMD. |
POSTHOG_OAUTH_ISSUER | https://oauth.posthog.com | Region-agnostic issuer; must be an absolute URL. |
Production publishes its PostHog CIMD at
https://www.workonrepeat.com/posthog-oauth-client.json. The document declares
the exact callback https://www.workonrepeat.com/oauth/callback/posthog and a
least-privilege scope ceiling. Set POSTHOG_OAUTH_CLIENT_ID to that document
URL and leave POSTHOG_OAUTH_CLIENT_SECRET unset. PostHog fetches the document
during authorization, and PKCE protects the code exchange.
Register each callback as ${UI_ORIGIN}/oauth/callback/<provider>.
The callback belongs to the UI origin rather than the API: the session cookie is
set through the UI's proxy and is host-only to it, so a provider redirecting
straight to the API would arrive without one.
Zernio needs no deployment configuration — each project pastes its own API key.
These variables configure Tools. In particular,
GITHUB_OAUTH_CLIENT_ID does not grant GitHub Copilot model access.
Connected model providers
Each connected-model provider has an independent, default-off flag. A flag must be enabled deliberately on both the API and the separately deployed private runtime only after that provider's rollout gate passes.
| Variable | Default | Notes |
|---|---|---|
MODEL_PROVIDER_OPENAI_CODEX_ENABLED | false | Enables ChatGPT via Codex routes and the official runtime adapter. |
MODEL_PROVIDER_GITHUB_COPILOT_ENABLED | false | Enables the dedicated Copilot model OAuth flow and adapter. |
MODEL_PROVIDER_ANTHROPIC_AGENT_ENABLED | false | Enables request-body Anthropic API-key connection and adapter. |
GITHUB_COPILOT_CLIENT_ID | — | Dedicated public-client OAuth app; never reuse GitHub Tools OAuth. |
GITHUB_COPILOT_CLIENT_SECRET | — | Dedicated Copilot OAuth secret. |
The API/runtime channel uses:
| Variable | Default | Notes |
|---|---|---|
AGENT_RUNTIME_URL | — | Canonical private runtime origin; production uses TLS and API-only ingress. |
AGENT_RUNTIME_SHARED_SECRET | — | At least 32 bytes; signs and encrypts runtime envelopes. |
AGENT_RUNTIME_REQUEST_TIMEOUT_MS | — | Positive bounded API-to-runtime request timeout. |
PORT | 3001 | Direct runtime service listen port in the runtime service environment. |
AGENT_RUNTIME_PORT | 3004 | Local monorepo wrapper/host port only; the runtime itself does not read this variable. |
RUNTIME_SANDBOX_COMMAND | — | Absolute path; required in production and whenever an official provider runs. |
RUNTIME_EGRESS_PROXY_URL | — | Canonical private Unix socket; required for every official provider. |
RUNTIME_MAX_CONCURRENT_RUNS | 4 | Positive per-replica capacity. Version one must still have exactly one replica. |
RUNTIME_TEMP_ROOT | /runtime-tmp/capabilities | Owned capability/session root on the noexec,nosuid temporary volume. |
CODEX_CLI_PATH | — | Absolute path required when the Codex provider is enabled. |
COPILOT_CLI_PATH | — | Absolute path required when the Copilot provider is enabled. |
The runtime's direct OCI/service configuration reads PORT, defaulting to
3001. The local monorepo dev wrapper maps AGENT_RUNTIME_PORT (default
3004) into that process's PORT. Docker Compose instead sets container
PORT=3001 and publishes the host mapping 3004:3001. The API is a separate
service with its own service-scoped PORT; do not use AGENT_RUNTIME_PORT to
configure a directly deployed runtime.
MODEL_PROVIDER_FAKE_ENABLED and MODEL_PROVIDER_FAKE_PROVIDERS are for the
explicit local/stub profile only; production rejects the fake provider. There
is no flag combination that authorizes automatic fallback between a personal
provider and LLMGateway.
The *_LIVE_SMOKE_DIAGNOSTIC_ENABLED switches and matching
*_DEPLOYMENT_ENVIRONMENT/*_DEPLOYMENT_ATTESTATION values are separately
default-off, nonproduction-only rollout inputs. They do not approve a target;
the exact target must also exist in the reviewed checked-in allowlist. See
Deployment before using any smoke setting.
API — managed tool providers and result delivery
Managed MCP providers and Discord delivery have independent, server-side
rollout flags. Every flag defaults to false; disabled integrations remain
listed as unavailable, and their new OAuth, API-key, create, or test routes
fail closed. Existing saved connections remain readable and manageable for
recovery.
| Variable | Default | Enables |
|---|---|---|
MANAGED_MCP_CUSTOMERIO_ENABLED | false | Customer.io US/EU OAuth-only connection beta. |
MANAGED_MCP_CLAY_ENABLED | false | Clay OAuth-only connection beta. |
MANAGED_MCP_CLOSE_ENABLED | false | Close connection-only beta and read-scoped setup. |
MANAGED_MCP_LINEAR_ENABLED | false | Linear catalog and new OAuth/API-key setup. |
MANAGED_MCP_FIRECRAWL_ENABLED | false | Firecrawl read-only beta catalog and new setup. |
MANAGED_MCP_EXA_ENABLED | false | Exa anonymous/API-key beta catalog and new setup. |
MANAGED_MCP_SEMRUSH_ENABLED | false | Semrush connection-only beta catalog and new setup. |
MANAGED_MCP_STRIPE_ENABLED | false | Stripe catalog and new OAuth/restricted-key setup. |
MANAGED_MCP_RESEND_ENABLED | false | Resend catalog and new OAuth/API-key setup. |
MANAGED_MCP_SENTRY_ENABLED | false | Sentry read-only beta catalog and new setup. |
MANAGED_MCP_SUPABASE_ENABLED | false | Supabase non-production read-only connection beta. |
MANAGED_MCP_SLACK_ENABLED | false | Slack OAuth-only connection beta (read scopes). |
MANAGED_MCP_HUBSPOT_ENABLED | false | HubSpot OAuth-only connection beta. |
MANAGED_MCP_NOTION_ENABLED | false | Notion OAuth-only connection beta. |
MANAGED_MCP_ATLASSIAN_ENABLED | false | Atlassian (Jira/Confluence) connection beta. |
MANAGED_MCP_CALCOM_ENABLED | false | Cal.com OAuth-only connection beta. |
MANAGED_MCP_INTERCOM_ENABLED | false | Intercom OAuth-only connection beta. |
MANAGED_MCP_APOLLO_ENABLED | false | Apollo OAuth-only connection beta (search and read scopes). |
MANAGED_MCP_PLAIN_ENABLED | false | Plain OAuth-only connection beta. |
MANAGED_MCP_PYLON_ENABLED | false | Pylon OAuth-only connection beta. |
MANAGED_MCP_ATTIO_ENABLED | false | Attio OAuth-only connection beta. |
MANAGED_MCP_GRANOLA_ENABLED | false | Granola OAuth-only connection beta (per-person grants). |
MANAGED_MCP_TWENTY_ENABLED | false | Twenty CRM OAuth-only connection beta (Twenty Cloud only). |
MANAGED_MCP_RETOOL_ENABLED | false | Retool OAuth-only connection beta (mcp:read). |
MANAGED_MCP_GOOGLE_DRIVE_ENABLED | false | Google Drive read-only connection beta (Developer Preview). |
MANAGED_MCP_GMAIL_ENABLED | false | Gmail read-only connection beta (Developer Preview). |
MANAGED_MCP_GOOGLE_CALENDAR_ENABLED | false | Google Calendar read-only connection beta (Developer Preview). |
MANAGED_MCP_SALESFORCE_ENABLED | false | Salesforce read-only hosted MCP connection beta. |
MANAGED_MCP_XERO_ENABLED | false | Xero OAuth-only connection beta (accounting read scopes). |
MANAGED_MCP_PIPEDRIVE_ENABLED | false | Pipedrive OAuth-only connection beta. |
MANAGED_MCP_APIFY_ENABLED | false | Apify OAuth or API-token connection beta. |
MANAGED_MCP_LOOPS_ENABLED | false | Loops OAuth-only connection beta. |
MANAGED_RESULT_DISCORD_ENABLED | false | New Discord destinations and connection-test sends. |
Clay reuses its configured OAuth client instead of dynamically registering a
new client for each authorization. It requires CLAY_MCP_OAUTH_CLIENT_ID when
the rollout flag is enabled in production; its matching
CLAY_MCP_OAUTH_CLIENT_SECRET is optional.
Slack and HubSpot advertise no dynamic client registration at all, so
their configured clients are hard requirements: enabling
MANAGED_MCP_SLACK_ENABLED in production refuses to boot without both
SLACK_MCP_OAUTH_CLIENT_ID and SLACK_MCP_OAUTH_CLIENT_SECRET, and
MANAGED_MCP_HUBSPOT_ENABLED refuses without both
HUBSPOT_MCP_OAUTH_CLIENT_ID and HUBSPOT_MCP_OAUTH_CLIENT_SECRET
(both token endpoints accept client_secret_post only). Either provider
stays unconfigured in the catalog — with unavailableReason: "client_unavailable" — until both halves exist, on every deployment.
Google Drive, Gmail, and Google Calendar share one Google Cloud OAuth client,
because accounts.google.com offers no dynamic client registration and the
three Google Workspace MCP servers are a Developer Preview. Config refuses to
enable MANAGED_MCP_GOOGLE_DRIVE_ENABLED, MANAGED_MCP_GMAIL_ENABLED, or
MANAGED_MCP_GOOGLE_CALENDAR_ENABLED in production without both
GOOGLE_MCP_OAUTH_CLIENT_ID and GOOGLE_MCP_OAUTH_CLIENT_SECRET. Register
${UI_ORIGIN}/oauth/callback/google-drive, …/gmail, and
…/google-calendar as that client's redirect URIs, and expect Google's app
verification requirements for the restricted gmail.readonly scope before a
production client leaves testing.
Salesforce, Xero, and Loops also require configured clients. Salesforce's
hosted MCP servers authorize through login.salesforce.com with an External
Client App (PKCE, no open dynamic registration); Xero's identity.xero.com
requires a registered Xero app; Loops accepts Client ID Metadata Documents or
preregistered clients rather than open dynamic registration. Config refuses to
enable MANAGED_MCP_SALESFORCE_ENABLED, MANAGED_MCP_XERO_ENABLED, or
MANAGED_MCP_LOOPS_ENABLED in production without the matching
SALESFORCE_MCP_OAUTH_CLIENT_ID, XERO_MCP_OAUTH_CLIENT_ID, or
LOOPS_MCP_OAUTH_CLIENT_ID and its _MCP_OAUTH_CLIENT_SECRET.
Close, Apollo, Plain, Pylon, Attio, Granola, Twenty CRM, Retool, Pipedrive,
and Apify advertise dynamic client registration, so they need no static OAuth
client variables. Optional static OAuth client values for other providers are
fallbacks only when the provider does not advertise dynamic client
registration: LINEAR_MCP_OAUTH_CLIENT_ID, STRIPE_MCP_OAUTH_CLIENT_ID,
RESEND_MCP_OAUTH_CLIENT_ID, SENTRY_MCP_OAUTH_CLIENT_ID,
NOTION_MCP_OAUTH_CLIENT_ID, ATLASSIAN_MCP_OAUTH_CLIENT_ID,
CALCOM_MCP_OAUTH_CLIENT_ID, INTERCOM_MCP_OAUTH_CLIENT_ID,
APOLLO_MCP_OAUTH_CLIENT_ID, PLAIN_MCP_OAUTH_CLIENT_ID,
PYLON_MCP_OAUTH_CLIENT_ID, ATTIO_MCP_OAUTH_CLIENT_ID,
GRANOLA_MCP_OAUTH_CLIENT_ID, TWENTY_MCP_OAUTH_CLIENT_ID,
RETOOL_MCP_OAUTH_CLIENT_ID, PIPEDRIVE_MCP_OAUTH_CLIENT_ID, and
APIFY_MCP_OAUTH_CLIENT_ID, each with an optional matching _CLIENT_SECRET;
they remain fallbacks when those servers do not advertise dynamic
registration.
API — team notifications
| Variable | Default | Notes |
|---|---|---|
DISCORD_NOTIFICATIONS_ENABLED | false | Enables delivery of queued account and billing events. Producers continue queuing while delivery is disabled. |
DISCORD_WEBHOOK_URL | — | Discord incoming-webhook URL (https://discord.com/api/webhooks/…). Required only when notifications are enabled. |
The API durably queues signup and Stripe billing lifecycle events, then the operations tick delivers them with bounded retries. A Discord outage never fails signup or billing, and disabling delivery preserves the backlog for a later rollout. Treat the webhook URL as a bearer secret — anyone holding it can post into the private channel. The private deployment runbook covers rollout, rotation, and backlog inspection procedures.
UI
| Variable | Default | Notes |
|---|---|---|
API_ORIGIN | — | Where the UI proxies /api/auth/* and /api/service/*. |
DOCS_ORIGIN | — | Where documentation links point. |
COMMERCIAL_ACQUISITION_ENABLED | false | Must match the API value before public launch. |
SECURITY_CONTACT_ENABLED | unset | Serves /.well-known/security.txt once inbound email works. |
Local development
.env.example holds local-only fixtures and is safe to copy:
cp .env.example .env
pnpm db:migrate
pnpm db:seed
pnpm devSQLITE_URL selects the local database file used by the Node API, migration,
and seed commands. Ploy production does not read it: Ploy provisions the DB
SQLite binding declared in apps/api/ploy.yaml and applies the committed
migrations under apps/api/migrations/DB.
Generate fresh BETTER_AUTH_SECRET, ENCRYPTION_KEY, and SCHEDULER_SECRET
values for every deployed environment. .env files are ignored by git and must
never be committed.