ReferenceOperating

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

VariableRule
API_ORIGINAbsolute URL of the API itself.
UI_ORIGINAbsolute URL of the UI. Also the only permitted CORS origin.
BETTER_AUTH_SECRETAt least 32 characters.
SCHEDULER_SECRETAt least 32 characters. Guards /internal/scheduler/tick.
ENCRYPTION_KEYBase64 that decodes to exactly 32 bytes.

Generate the encryption key with:

openssl rand -base64 32

API — client address behind a proxy

VariableDefaultRule
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::/32

The 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

VariableDefaultRule
LLM_GATEWAY_API_KEY—Required unless DEMO_MODE=true. Never required for the public model directory.
LLM_GATEWAY_DEFAULT_MODELopenai/gpt-5-miniNon-empty string.
DEMO_MODEfalsetrue or false. Rejected outright in production.
NODE_ENVdevelopmentdevelopment, production, or test.

Startup fails when:

  • NODE_ENV=production and DEMO_MODE=true;
  • LLM_GATEWAY_API_KEY is absent and DEMO_MODE is not true;
  • ENCRYPTION_KEY does not decode to 32 bytes; or
  • the Node entrypoint runs with NODE_ENV=production and no AUTH_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.

VariableDefaultRule
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_ENABLEDfalseSet true only after launch approval. false blocks only new Checkout sessions.
STRIPE_AUTOMATIC_TAX_ENABLEDfalseIndependently 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.

VariableDefaultRule
AUTH_EMAIL_CAPTURE_ENABLEDfalseTest/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.

VariableDefaultNotes
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_ISSUERhttps://oauth.posthog.comRegion-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.

VariableDefaultNotes
MODEL_PROVIDER_OPENAI_CODEX_ENABLEDfalseEnables ChatGPT via Codex routes and the official runtime adapter.
MODEL_PROVIDER_GITHUB_COPILOT_ENABLEDfalseEnables the dedicated Copilot model OAuth flow and adapter.
MODEL_PROVIDER_ANTHROPIC_AGENT_ENABLEDfalseEnables 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:

VariableDefaultNotes
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.
PORT3001Direct runtime service listen port in the runtime service environment.
AGENT_RUNTIME_PORT3004Local 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_RUNS4Positive per-replica capacity. Version one must still have exactly one replica.
RUNTIME_TEMP_ROOT/runtime-tmp/capabilitiesOwned 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.

VariableDefaultEnables
MANAGED_MCP_CUSTOMERIO_ENABLEDfalseCustomer.io US/EU OAuth-only connection beta.
MANAGED_MCP_CLAY_ENABLEDfalseClay OAuth-only connection beta.
MANAGED_MCP_CLOSE_ENABLEDfalseClose connection-only beta and read-scoped setup.
MANAGED_MCP_LINEAR_ENABLEDfalseLinear catalog and new OAuth/API-key setup.
MANAGED_MCP_FIRECRAWL_ENABLEDfalseFirecrawl read-only beta catalog and new setup.
MANAGED_MCP_EXA_ENABLEDfalseExa anonymous/API-key beta catalog and new setup.
MANAGED_MCP_SEMRUSH_ENABLEDfalseSemrush connection-only beta catalog and new setup.
MANAGED_MCP_STRIPE_ENABLEDfalseStripe catalog and new OAuth/restricted-key setup.
MANAGED_MCP_RESEND_ENABLEDfalseResend catalog and new OAuth/API-key setup.
MANAGED_MCP_SENTRY_ENABLEDfalseSentry read-only beta catalog and new setup.
MANAGED_MCP_SUPABASE_ENABLEDfalseSupabase non-production read-only connection beta.
MANAGED_MCP_SLACK_ENABLEDfalseSlack OAuth-only connection beta (read scopes).
MANAGED_MCP_HUBSPOT_ENABLEDfalseHubSpot OAuth-only connection beta.
MANAGED_MCP_NOTION_ENABLEDfalseNotion OAuth-only connection beta.
MANAGED_MCP_ATLASSIAN_ENABLEDfalseAtlassian (Jira/Confluence) connection beta.
MANAGED_MCP_CALCOM_ENABLEDfalseCal.com OAuth-only connection beta.
MANAGED_MCP_INTERCOM_ENABLEDfalseIntercom OAuth-only connection beta.
MANAGED_MCP_APOLLO_ENABLEDfalseApollo OAuth-only connection beta (search and read scopes).
MANAGED_MCP_PLAIN_ENABLEDfalsePlain OAuth-only connection beta.
MANAGED_MCP_PYLON_ENABLEDfalsePylon OAuth-only connection beta.
MANAGED_MCP_ATTIO_ENABLEDfalseAttio OAuth-only connection beta.
MANAGED_MCP_GRANOLA_ENABLEDfalseGranola OAuth-only connection beta (per-person grants).
MANAGED_MCP_TWENTY_ENABLEDfalseTwenty CRM OAuth-only connection beta (Twenty Cloud only).
MANAGED_MCP_RETOOL_ENABLEDfalseRetool OAuth-only connection beta (mcp:read).
MANAGED_MCP_GOOGLE_DRIVE_ENABLEDfalseGoogle Drive read-only connection beta (Developer Preview).
MANAGED_MCP_GMAIL_ENABLEDfalseGmail read-only connection beta (Developer Preview).
MANAGED_MCP_GOOGLE_CALENDAR_ENABLEDfalseGoogle Calendar read-only connection beta (Developer Preview).
MANAGED_MCP_SALESFORCE_ENABLEDfalseSalesforce read-only hosted MCP connection beta.
MANAGED_MCP_XERO_ENABLEDfalseXero OAuth-only connection beta (accounting read scopes).
MANAGED_MCP_PIPEDRIVE_ENABLEDfalsePipedrive OAuth-only connection beta.
MANAGED_MCP_APIFY_ENABLEDfalseApify OAuth or API-token connection beta.
MANAGED_MCP_LOOPS_ENABLEDfalseLoops OAuth-only connection beta.
MANAGED_RESULT_DISCORD_ENABLEDfalseNew 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

VariableDefaultNotes
DISCORD_NOTIFICATIONS_ENABLEDfalseEnables 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

VariableDefaultNotes
API_ORIGIN—Where the UI proxies /api/auth/* and /api/service/*.
DOCS_ORIGIN—Where documentation links point.
COMMERCIAL_ACQUISITION_ENABLEDfalseMust match the API value before public launch.
SECURITY_CONTACT_ENABLEDunsetServes /.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 dev

SQLITE_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.

On this page

Edit this page on GitHub