Configuration reference
Every environment variable opendesk and the realtime gateway read, with defaults and meaning.
opendesk reads its configuration from environment variables. The self-host bundle keeps them in /opt/opendesk/.env; for local development, copy .env.example in the repository to .env.
Empty values use the default. Booleans accept true/false, 1/0.
Refuses to boot
Section titled “Refuses to boot”opendesk checks its secrets at start and refuses to run when they are unsafe:
AUTH_SECRETshorter than 32 bytes. Outside production a development secret is used when it is empty.CREDENTIALS_KEYmissing in production, or not 32 bytes.- In production, embedded NATS listening beyond
127.0.0.1withoutNATS_TOKEN. OPENDESK_ROLEother thanall,apiorworker.STRIPE_SECRET_KEYset withoutSTRIPE_WEBHOOK_SECRETandSTRIPE_PRICE_PRO.- Only some of
APNS_KEY,APNS_KEY_IDandAPNS_TEAM_IDset,PUSH_RELAY_SERVE=truewithout an APNs key, or aPUSH_RELAY_URLthat is not anhttpsorigin.
The realtime gateway also refuses to start with an AUTH_SECRET under 32 bytes.
| Variable | Default | Meaning |
|---|---|---|
OPENDESK_ENV |
development |
development or production. Production enforces the checks above. |
OPENDESK_ROLE |
all |
all (API and workers), api or worker. See Scaling. |
HTTP_ADDR |
:8080 |
Address the HTTP server listens on. |
DATA_DIR |
./data |
Local data: attachments with the local driver, and embedded NATS storage. |
SERVE_WEB |
false |
Serve the embedded web app, /config.js and /config.json (for the desktop and iPhone apps) from this binary. true in the self-host bundle. |
SIGNUP_ENABLED |
true |
Allow anyone to register. When false, only invited people can create an account. |
TRUST_PROXY |
false |
Trust X-Forwarded-* headers from a reverse proxy for the client IP. Enable only behind a proxy you control. |
EVENT_RETENTION |
168h |
How long events are kept for replay (a Go duration). |
| Variable | Default | Meaning |
|---|---|---|
API_URL |
http://localhost:8080 |
Public URL of the API. OAuth redirect URIs are built from it. |
APP_URL |
http://localhost:5173 |
Public URL of the web app. Links in email point here. |
GATEWAY_URL |
ws://localhost:4000/socket |
Public WebSocket URL of the realtime gateway, given to clients. |
SITE_URL |
https://opendesk.sh |
Marketing site URL used in links. |
CORS_ORIGINS |
APP_URL |
Comma-separated origins allowed to call the API from a browser. |
COOKIE_DOMAIN |
empty (host only) | Domain for the refresh cookie. Set it when the API and app are on different subdomains of one domain. |
COOKIE_SECURE |
true when API_URL is https |
Mark cookies Secure. |
On a self-hosted single domain, API_URL and APP_URL are both https://desk.example.com and GATEWAY_URL is wss://desk.example.com/socket.
Database
Section titled “Database”| Variable | Default | Meaning |
|---|---|---|
DATABASE_URL |
postgres://opendesk:opendesk@localhost:5432/opendesk?sslmode=disable |
Pooled connection used by request handlers. On Neon, the -pooler host. |
DATABASE_URL_DIRECT |
DATABASE_URL |
Direct connection for migrations, background jobs and the event relay, which need session features such as LISTEN/NOTIFY. |
See Postgres and Neon.
Secrets
Section titled “Secrets”| Variable | Default | Meaning |
|---|---|---|
AUTH_SECRET |
none in production | At least 32 bytes. Signs every JWT. Must be identical in opendesk and the realtime gateway. |
CREDENTIALS_KEY |
none in production | 32 bytes, hex or base64. Encrypts mailbox credentials, OAuth tokens, webhook secrets and TOTP secrets with AES-256-GCM. Never change it: existing credentials become unreadable. |
| Variable | Default | Meaning |
|---|---|---|
NATS_URL |
empty | Empty runs NATS embedded in this process. Set it to use an external NATS cluster. |
NATS_TOKEN |
empty | Token for the embedded server, or for the external cluster. Required in production when embedded NATS listens beyond localhost. |
NATS_LISTEN |
127.0.0.1:4222 |
Where embedded NATS listens, so the realtime gateway can connect. |
NATS_STORE_DIR |
DATA_DIR/nats |
Embedded NATS storage directory. |
Storage
Section titled “Storage”| Variable | Default | Meaning |
|---|---|---|
STORAGE_DRIVER |
local |
local (under DATA_DIR) or s3 (any S3-compatible store). |
S3_BUCKET |
Bucket name. | |
S3_REGION |
auto |
Region. auto suits Cloudflare R2. |
S3_ENDPOINT |
Custom endpoint for R2, MinIO and others. Empty uses AWS. | |
S3_ACCESS_KEY_ID |
Access key. | |
S3_SECRET_ACCESS_KEY |
Secret key. | |
S3_FORCE_PATH_STYLE |
false |
Use path-style URLs. Usually needed for MinIO. |
See Storage.
Platform email
Section titled “Platform email”Verification, password reset and invitation emails. Replies to customers go through each mailbox, not this.
| Variable | Default | Meaning |
|---|---|---|
MAIL_DRIVER |
log |
log (print to the log), smtp or ses. |
MAIL_FROM |
opendesk <notifications@localhost> |
From address. |
SMTP_HOST |
SMTP server. | |
SMTP_PORT |
587 |
SMTP port. |
SMTP_USERNAME |
SMTP username. | |
SMTP_PASSWORD |
SMTP password. | |
SMTP_TLS |
starttls |
starttls, tls or none. |
SES_REGION |
us-east-1 |
Amazon SES region, with MAIL_DRIVER=ses. Credentials come from the standard AWS environment. |
EMAIL_LEGAL_ENTITY |
Company name for a line under every email’s footer, where the law asks for one. Unset shows no company line. | |
EMAIL_LEGAL_COMPANY_NUMBER |
Company number in that line. | |
EMAIL_LEGAL_PLACE_OF_REGISTRATION |
Where the company is registered, for example England and Wales. |
|
EMAIL_LEGAL_ADDRESS |
Registered office address. |
Notification emails
Section titled “Notification emails”How often notification emails may go to one person (see How emails are paced). Durations are Go durations: 90s, 5m, 1h.
| Variable | Default | Meaning |
|---|---|---|
NOTIFY_EMAIL_PACING |
relaxed (strict with billing on) |
The set of defaults the variables below change. |
NOTIFY_EMAIL_DELAY |
5m |
Wait after a notification before its email, so reading it in the app first skips the email. |
NOTIFY_EMAIL_COOLDOWNS |
5m,15m,30m,1h (15m,30m,1h) |
Gaps between emails while more keeps coming; the last one repeats. |
NOTIFY_EMAIL_QUIET_RESET |
2h |
Quiet time after which the gaps start over. |
NOTIFY_EMAIL_URGENT_GAP |
5m (10m) |
Shortest gap an urgent notification can cut a wait down to. |
NOTIFY_EMAIL_URGENT_PER_DAY |
12 (6) |
How many times a day urgent ones may cut a wait short. |
NOTIFY_EMAIL_PER_HOUR |
12 (4) |
Most emails per person in an hour, digests included. |
NOTIFY_EMAIL_PER_DAY |
100 (24) |
Most emails per person in a day. |
NOTIFY_EMAIL_INSTANCE_PER_MINUTE |
60 (120) |
Most notification emails the whole server sends in a minute; the rest wait. |
NOTIFY_EMAIL_ACTIVE_WINDOW |
5m |
Nothing is emailed while someone was active in the workspace this recently. |
Inbound email
Section titled “Inbound email”| Variable | Default | Meaning |
|---|---|---|
INBOUND_DOMAIN |
in.localhost |
Forwarding addresses are <token>@INBOUND_DOMAIN. |
INBOUND_SECRET |
Shared secret for POST /v1/inbound/email (the OD-Inbound-Secret header). Empty turns the endpoint off. |
|
INBOUND_SMTP_ADDR |
empty (off) | Address for the built-in SMTP receiver, such as :2525 or :25. |
INBOUND_AUTHSERV_ID |
empty | Comma-separated authserv-ids of the receiving servers whose Authentication-Results are believed, such as mx.cloudflare.net for the Cloudflare Email Worker. Only the first such header from the top counts. Gmail and Microsoft 365 mailboxes always trust their own provider. Leave empty with the built-in SMTP receiver: it does not check senders, so their DMARC result is unknown. |
See Email setup.
Sign-in providers and mailbox OAuth
Section titled “Sign-in providers and mailbox OAuth”| Variable | Default | Meaning |
|---|---|---|
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
Google sign-in and Gmail mailbox connections. | |
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET |
Microsoft sign-in and Outlook mailbox connections. | |
MICROSOFT_TENANT |
common |
common, organizations, consumers or your tenant id. |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
GitHub sign-in. | |
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET |
Any OpenID Connect provider for single sign-on. | |
OIDC_NAME |
SSO |
Button label for the OIDC provider. |
See Google and Microsoft OAuth. A sign-in provider is offered only when it is configured (for OIDC, the issuer and client id).
Mailbox sign-in
Section titled “Mailbox sign-in”Connecting a mailbox with Google or Microsoft sign-in has its own switch per provider. It is on when that provider’s client is configured, and it does not change how people sign in to opendesk.
| Variable | Default | Meaning |
|---|---|---|
GOOGLE_MAILBOX_OAUTH |
true when GOOGLE_CLIENT_ID is set |
false stops offering Google sign-in for new mailboxes. Mailboxes already connected with it keep syncing and can reconnect. |
MICROSOFT_MAILBOX_OAUTH |
true when MICROSOFT_CLIENT_ID is set |
The same for Microsoft. |
MAILHOST_ISPDB |
false (true with billing configured) |
When detecting a mailbox’s servers, also look up unknown domains in Thunderbird’s public ISPDB (autoconfig.thunderbird.net). The domain’s own autoconfig, MX and SRV records are always used. |
With a provider’s mailbox sign-in off, or its client not configured, the mailbox wizard offers the other ways in: Gmail and Google Workspace connect with an app password over IMAP and SMTP, any provider can forward its mail into opendesk, and other providers connect over IMAP and SMTP with the settings the wizard detects. A Microsoft mailbox without sign-in uses forwarding, and replies go out through opendesk or the mailbox’s SMTP server when the tenant allows SMTP AUTH.
A value that is not true or false (or 1 and 0) turns the switch off. The API answers a new connection with 403 mailbox_oauth_disabled, and the web app reads the same list from its runtime config (mailboxOAuth).
Profile pictures
Section titled “Profile pictures”| Variable | Default | Meaning |
|---|---|---|
LOGO_DEV_TOKEN |
A logo.dev publishable token. When set, company logos come from logo.dev instead of the company’s website. |
Agent pictures from Google and GitHub, Gravatar and website icons need no configuration. See Profile pictures and logos.
Bot protection and analytics
Section titled “Bot protection and analytics”| Variable | Default | Meaning |
|---|---|---|
TURNSTILE_SITE_KEY, TURNSTILE_SECRET |
Cloudflare Turnstile on login, registration and password reset. | |
POSTHOG_KEY |
Product analytics. Empty sends nothing. | |
POSTHOG_HOST |
https://us.i.posthog.com |
PostHog host. |
AI features are on when a provider is configured. See AI features.
| Variable | Default | Meaning |
|---|---|---|
AI_PROVIDER |
detected | anthropic, openai (any OpenAI-compatible endpoint) or none. When empty: anthropic if ANTHROPIC_API_KEY is set, else openai if OPENAI_API_KEY is set, else none. |
ANTHROPIC_API_KEY |
Anthropic API key. | |
OPENAI_API_KEY |
Key for OpenAI or a compatible endpoint. | |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
Any OpenAI-compatible endpoint: Azure, Ollama, vLLM, LM Studio. Point it at a local model to keep everything on your server. |
AI_MODEL |
claude-sonnet-5-5 with Anthropic |
Model for drafting, summaries and answers. Required for openai. |
AI_FAST_MODEL |
claude-haiku-4-5-20251001 with Anthropic, else AI_MODEL |
Model for triage, classification and short rewrites. |
EMBEDDINGS_DRIVER |
detected | voyage, openai or none. When empty: voyage if VOYAGE_API_KEY is set, else openai with the openai provider, else none (search falls back to Postgres full-text search). |
VOYAGE_API_KEY |
Voyage AI key for embeddings. | |
EMBEDDINGS_MODEL |
Embeddings model. | |
DECISIONS_PROVIDER |
detected | Engine for typed decisions (triage, the chat answer bot’s relevance check, ai_decide automation conditions): typesafe (Jev by TypeSafe), llm (the AI provider’s fast model) or none. When empty: typesafe if TYPESAFE_API_KEY is set, else llm if an AI provider is configured, else none. |
TYPESAFE_API_KEY |
TypeSafe API key for Jev. Without it, decisions use the LLM path and every feature keeps working. | |
TYPESAFE_API_URL |
https://api.typesafe.ai |
TypeSafe API base URL. For the Vercel AI Gateway use https://ai-gateway.vercel.sh/typesafe with TYPESAFE_MODEL=typesafe-ai/jev. |
TYPESAFE_MODEL |
jev-latest |
Jev model or alias. Pin a version such as jev-1.13.0 to keep answers stable across releases. |
Semantic search needs the Postgres vector extension (pgvector). Without it, opendesk uses full-text search.
Integrations
Section titled “Integrations”Connected products for Ollie (MCP servers). Most sign in without any setup; see Integrations.
| Variable | Default | Meaning |
|---|---|---|
MCP_<KEY>_CLIENT_ID |
An OAuth client registered in advance with a product that does not accept self-registration, for example MCP_SLACK_CLIENT_ID. Redirect URL: <API_URL>/v1/integrations/oauth/callback. |
|
MCP_<KEY>_CLIENT_SECRET |
That client’s secret, when the product issued one. |
Native apps
Section titled “Native apps”Settings for the desktop and iPhone apps. The desktop app needs none of them. See Push notifications for choosing a push mode.
| Variable | Default | Meaning |
|---|---|---|
PUSH_RELAY_URL |
empty (off) | Send iPhone pushes through this relay when there is no APNs key: an https origin, such as https://api.opendesk.sh. The relay sees this instance’s address, the notification type and the ticket number, never ticket text, names or email addresses. |
APNS_KEY |
empty | APNs auth key: the .p8 file as PEM text or base64. With a key, the instance sends pushes itself (direct mode). |
APNS_KEY_ID |
The key’s id. Required with APNS_KEY. |
|
APNS_TEAM_ID |
The Apple developer team that owns the key and signs the app. Required with APNS_KEY. |
|
APNS_TOPIC |
sh.opendesk.app |
The bundle id of the app that receives pushes. |
APP_ATTEST_APP_IDS |
empty (off) | Comma-separated app ids (TEAMID.bundle) whose App Attest proofs are accepted in place of Turnstile on login, registration and password reset. Needed for the iPhone app only when Turnstile is on. |
APP_ATTEST_DEVELOPMENT |
false |
Also accept App Attest keys from development builds (run from Xcode). Leave off in production. |
PUSH_RELAY_SERVE |
false |
Cloud only: serve the push relay (/v1/push/relay/*) for other instances. Needs an APNs key. |
Push mode follows from these: an APNs key means direct, else PUSH_RELAY_URL means relay, else off. GET /v1/auth/config reports it under native.push.
Every instance answers GET /v1/push/instance-key with the public half of a key derived from AUTH_SECRET, which the relay uses to check that pushes come from this instance. Changing AUTH_SECRET changes it.
Billing (Cloud only)
Section titled “Billing (Cloud only)”Leave these empty on a self-hosted instance. Without STRIPE_SECRET_KEY, every workspace is on the self_hosted plan with no limits and unlimited AI.
| Variable | Default | Meaning |
|---|---|---|
STRIPE_SECRET_KEY |
Turns billing on. | |
STRIPE_WEBHOOK_SECRET |
Verifies Stripe webhooks. Required with billing. | |
STRIPE_PRICE_PRO |
Monthly per-seat price. Required with billing. | |
STRIPE_PRICE_PRO_YEARLY |
Optional yearly per-seat price. | |
STRIPE_PRICE_AI_TOPUP |
One-time $10 price for an AI top-up, which adds $10 of AI usage. STRIPE_PRICE_CREDITS is still read when this is unset. |
|
AI_USAGE_MARKUP |
1.5 |
What each AI call is charged, as a multiple of its provider cost (1 to 100). Read only when billing is on. AI_CREDIT_MARKUP is still read when this is unset. |
STRIPE_TAX |
true |
Automatic tax on Checkout. |
STRIPE_API_URL |
Development only: a local Stripe fake or stripe-mock. Refused when OPENDESK_ENV=production. |
Realtime gateway
Section titled “Realtime gateway”The realtime container reads its own variables:
| Variable | Default | Meaning |
|---|---|---|
PORT |
4000 |
Listen port. |
AUTH_SECRET |
The same value as opendesk. Verifies socket tokens. | |
NATS_URL |
NATS to subscribe to: opendesk’s embedded server (NATS_LISTEN) or your cluster. |
|
NATS_TOKEN |
NATS token. | |
CHECK_ORIGINS |
Comma-separated browser origins allowed to open agent sockets, such as https://desk.example.com. The chat widget endpoint (/socket/widget) accepts any origin, since it takes only widget tokens. |
|
SECRET_KEY_BASE |
Phoenix secret. Generate with openssl rand -hex 48. |
|
CLUSTER_DNS |
empty | A DNS name that resolves to every gateway node, so Presence spans nodes. A single node needs nothing. |
POSTHOG_KEY, POSTHOG_HOST |
Optional connection metrics and errors. |
The gateway’s health check is GET /health.