WarmblyDocs

Configuration reference

Every environment variable Warmbly reads, what it does, its default, and whether changing it needs a restart.

The environment is authoritative. Warmbly never lets a web form overwrite a setting your environment owns, so there is no precedence to reason about and no file that silently rewrites itself behind you.

That rule has three consequences worth stating before the tables:

  • Everything on this page is set in the environment of the running process. With Docker Compose that is the .env next to docker-compose.yml. With Kubernetes it is the pod spec or a secret. With a bare binary it is the shell or the systemd unit.
  • Most resolved values are visible, read only, in the admin panel under Instance > Configuration (http://localhost:5174/configuration on a stock install). Each row shows the variable name, the value Warmbly actually resolved, where it came from (env, default, derived or unset), and whether changing it needs a restart. That page is how you answer "is my variable actually being picked up", without reading source.
  • A handful of settings are stored in the database instead, because no environment variable owns them. They are listed in settings stored in the database and are the only settings editable from a browser.

Secrets are never returned by any API. The configuration page shows a sensitive key as set or unset plus a four character fingerprint of its value, which is enough to confirm that two services hold the same AUTH_SECRET without disclosing it to anyone.

The panel reads the backend, not the whole fleet

The configuration registry runs inside the backend process, so every value it shows is the value that process resolved. It does not reach into the realtime, tracking, consumer or worker containers, and it does not list variables only those services read: realtime service and tracking service are absent from the page entirely. When a value has to match across services, compare the fingerprints or read the other container's environment directly.

Seeing what is actually set

HowWhat you get
Instance > Configuration in the admin panelThe backend's entries with resolved value, source, group and restart requirement
make doctorThe health checks from a shell, including the configuration problems they detect
GET /admin/instance/configThe same list as JSON, behind the manage_settings admin permission

Anything flagged on Instance health links back to the section of this page that explains the fix.

An empty value in .env is not an empty value

docker-compose.yml reads this file as ${VAR:-default}, and Compose treats an empty assignment exactly like a missing one. KMS_LOCAL_MASTER_KEY= does not blank the key, it substitutes the published default. To leave something unset under Compose, comment the line out.

Deployment

VariableWhat it doesDefaultRestart needed
APP_ENVdev or prod. dev tolerates the published default secrets and turns on Gin debug logging. Set prod for anything other people can reachdevyes
DEPLOYMENT_MODEself_hosted or cloud. Picks the auth defaults in authentication; every one stays individually overridableself_hosted under composeyes
ALLOW_INSECURE_DEFAULTStrue lets the backend boot even when a secret still holds its published default. Only for a throwaway instanceunsetyes
GIN_MODEdebug or releasereleaseyes
ENV_LABELA label the admin panel shows next to the instance nameunsetyes (container start)
WARMBLY_ALLOW_UNSAFE_WEBHOOK_URLStrue lets customer webhooks point at http:// and private addresses. Development only: it lets any workspace member make the backend reach into your internal networkfalseyes

prod does not mean cloud

APP_ENV=prod needs no cloud account. Error reporting and GeoIP lookups are used when configured and skipped with a logged note when they are not.

Secrets

Five values protect the whole instance. Compose ships a working default for each so a fresh clone boots with no configuration, and every one of those defaults is published in this repository, so they protect nothing.

VariableFormatWhat it protectsRestart needed
AUTH_SECRET32 characters or moreJWT and session signing. The realtime service reads the same value as JWT_SECRETyes
INTERNAL_API_TOKENany random stringThe backend's /api/v1/internal/ routes, which workers and the tracking service authenticate againstyes
SECRET_KEY_BASE64 characters or morePhoenix session signing in the realtime serviceyes
KMS_LOCAL_MASTER_KEYbase64, exactly 32 bytesThe root key that seals every per-organization data keyyes
CREDENTIALS_ENCRYPTION_KEYexactly 64 hex charactersMailbox credentials at rest: SMTP and IMAP passwords, and Gmail and Outlook OAuth access and refresh tokensyes

Generate real ones before anyone else can reach the instance:

cat >> .env <<EOF
AUTH_SECRET=$(openssl rand -base64 32)
INTERNAL_API_TOKEN=$(openssl rand -hex 24)
SECRET_KEY_BASE=$(openssl rand -base64 64 | tr -d '\n')
KMS_LOCAL_MASTER_KEY=$(openssl rand -base64 32)
CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -hex 32)
APP_ENV=prod
EOF

make gen-key prints a single fresh KMS_LOCAL_MASTER_KEY if that is all you need.

APP_ENV=prod goes last. It is what turns a published default from a logged warning into a refusal to start, so an instance that gets prod before the other five will not boot.

Values that must be identical across services, because each service reads its own copy:

ValueRead byIf it drifts
AUTH_SECRET, seen by realtime as JWT_SECRETbackend, realtimeThe dashboard loads but never goes live: the websocket rejects every token
INTERNAL_API_TOKEN, seen by workers as ENCRYPTED_KEYS_WORKER_TOKENbackend, worker, trackingWorkers cannot fetch decryption keys and tracking cannot resolve click tickets. Both fail closed with 401
KMS_LOCAL_MASTER_KEYbackend, consumer, workerSealed data keys cannot be opened, so mailbox credentials stop decrypting
CREDENTIALS_ENCRYPTION_KEYbackend, workerStored SMTP and IMAP passwords and OAuth tokens stop decrypting, so no mailbox can send or sync

Only the backend refuses to boot on a published default

The secret check runs in the backend. The consumer and the workers start happily on a published default, so an instance can look healthy while one process is using a key anyone can read from GitHub. The secret_published_default check on Instance health is what catches it.

Back up the two encryption keys

KMS_LOCAL_MASTER_KEY and CREDENTIALS_ENCRYPTION_KEY seal every stored credential and every stored message body. Losing them is unrecoverable, and a database backup without them cannot be decrypted.

Addresses

Every emailed link (password reset, invitation, the first-run claim link) is built from APP_URL. Leave it unset and those links are built against the hosted service, which means a live reset token leaves your deployment.

VariableWhat it doesDefaultRestart needed
APP_URLThe dashboard origin. The source of every emailed linkhttps://app.warmbly.comno (read per request)
FRONTEND_BASE_URLAlternative name for the same value, read when APP_URL is unsetunsetno
API_PUBLIC_URLThe backend's public base. Frontends, blob URLs and the OIDC redirect derive from itderived from PUBLIC_HOST under composeyes
BACKEND_PUBLIC_URLThe backend base used in generated worker configurationfalls back to API_PUBLIC_URLyes
APP_ORIGINThe exact origin the mailbox OAuth callback page posts the authorization code back to. Only needed when the dashboard is served somewhere other than APP_URLderived from APP_URLyes
API_HOSTThe listen address0.0.0.0:8080yes
PUBLIC_HOSTCompose only. A hostname or LAN IP that every other URL derives fromlocalhostyes
CORS_ALLOW_ORIGINSComma separated origins allowed to call the API. Anything not listed gets 403 on preflightderived from PUBLIC_HOST under composeyes
WEBSOCKET_URLThe websocket URL the dashboard connects toderived under composeyes (container start)
PHX_HOSTThe realtime service's own hostnamelocalhostyes
TRACKING_DOMAINThe domain that serves open pixels and click links. Use a separate, neutral domain in productionlocalhost:3000no
TRACKING_SERVICE_URLWhere the backend reaches the tracking service internallyunsetyes

Setting PUBLIC_HOST turns localhost off

Once PUBLIC_HOST is set, every derived URL uses it and http://localhost:5173 stops working, because a localhost origin is no longer in CORS_ALLOW_ORIGINS. To keep both, list them yourself in CORS_ALLOW_ORIGINS.

Network and proxy

VariableWhat it doesDefaultRestart needed
TRUSTED_PROXIESComma separated CIDRs allowed to set X-Forwarded-Forempty (trust nothing)yes

Empty is correct for a directly exposed backend. Behind a reverse proxy it is not: with no trusted CIDR, Warmbly records the proxy's address as the client address, and the per IP login limiter, session records, audit rows and API key IP allowlists all read the wrong address. Set it to the CIDR your proxy connects from:

TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12

Authentication

VariableWhat it doesDefaultRestart needed
AUTH_LOGIN_CODEalways, new_device or off. Whether a login also requires a code emailed to the accountoff on self-host, new_device on cloudyes
REQUIRE_EMAIL_VERIFICATIONWhether a signup must confirm an emailed code before the account existsfalse on self-hostyes
DISABLE_REGISTRATIONfalse, invite_only or true. See registration modesinvite_only on self-hostyes
DISABLE_PASSWORD_LOGINTurns off email and password entirely, for single sign-on only deploymentsfalseyes
SSO_AUTO_PROVISIONtrue lets a verified identity provider assertion create an account regardless of DISABLE_REGISTRATIONfalseyes
AUTH_IP_RATE_LIMITUnauthenticated auth requests allowed per source IP per 15 minutes60yes
WARMBLY_BOOTSTRAP_EMAILFirst owner's address, read only while the users table is emptyunsetyes
WARMBLY_BOOTSTRAP_PASSWORD_HASHArgon2 PHC string for that owner. Preferred over the plaintext formunsetyes
WARMBLY_BOOTSTRAP_PASSWORDPlaintext convenience form. Warns at boot, and leaves a password in your process environmentunsetyes
WARMBLY_BOOTSTRAP_ORGName of the organization created with that ownerderived from the nameyes
TWOFA_SECRETKey that encrypts stored TOTP secrets. Falls back to AUTH_SECRET, so existing deployments keep working; rotating it invalidates every enrolled TOTP secretAUTH_SECRETyes
WEBAUTHN_RP_IDPasskey relying party id. Derived from APP_URL when unset. Changing it invalidates every enrolled passkeyderivedyes
WEBAUTHN_RP_ORIGINSOrigins accepted for passkey ceremoniesderived from APP_URLyes
WEBAUTHN_RP_DISPLAY_NAMEThe name the passkey prompt showsWarmblyyes
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URISign in with Google. Unrelated to the BOX_GOOGLE_* mailbox clientunsetyes
GOOGLE_IOS_CLIENT_IDAdditional Google client id accepted from the iOS appunsetyes
APPLE_APP_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_KEY_SECRETSign in with Appleunsetyes
APPLE_IOS_BUNDLE_IDBundle id accepted from the iOS appcom.warmbly.appyes
OIDC_ISSUER_URLGeneric OpenID Connect issuer. Discovery runs at bootunsetyes
OIDC_CLIENT_ID, OIDC_CLIENT_SECRETThe client Warmbly authenticates asunsetyes
OIDC_REDIRECT_URLRedirect URI registered at the provider. Defaults to API_PUBLIC_URL plus /v1/auth/oidc/callbackderivedyes
OIDC_SCOPESScopes requested at the provideropenid,profile,emailyes
OIDC_ALLOWED_DOMAINSEmail domains allowed to sign in through the providerempty (any)yes
OIDC_DEFAULT_ORGOrganization uuid every single sign-on user joinsunsetyes
OIDC_PROVIDER_NAMEThe label on the sign-in buttonSingle sign-onyes

Full behavior, including what each registration mode does to the sign-up form, is on accounts and access.

Captcha

VariableWhat it doesDefaultRestart needed
CAPTCHA_PROVIDERnone or turnstilederived, see belowyes
TURNSTILE_SECRETCloudflare Turnstile secret, read by the backendunsetyes
WARMBLY_TURNSTILE_KEYThe Turnstile site key, read by the dashboard and admin panel at container starta test key under composeyes (container start)
TURNSTILE_BYPASS_TOKENA token that skips verification. Only honoured when APP_ENV=devunsetyes

CAPTCHA_PROVIDER has no constant default. Unset, it resolves to turnstile when TURNSTILE_SECRET holds a value and to none when it does not, so configuring the secret is what turns captcha on and clearing it is what turns captcha off. The panel reports the resolved value with source derived.

Setting CAPTCHA_PROVIDER=turnstile explicitly while TURNSTILE_SECRET is empty is the one combination that breaks: every verification fails, which means nobody can sign in. Set the secret or set the provider back to none.

Platform mail

Platform mail is the product's own outbound: registration codes, password resets, team invitations, notification digests and login codes where those are enabled. It is separate from campaign mail, which leaves through the mailboxes you connect.

VariableWhat it doesDefaultRestart needed
MAIL_TRANSPORTsmtp, log or seslog under compose, ses for a bare binary with no SMTP_HOSTyes
EMAIL_NAMEDisplay name on platform mailWarmblyyes
EMAIL_ADDRESSFrom address on platform mailnone, and the backend refuses to start without ityes
SMTP_HOSTRelay hostnameunsetyes
SMTP_PORTRelay port. Follows SMTP_SECURITY when unsetderivedyes
SMTP_USERNAME, SMTP_PASSWORDRelay credentials. Never sent over an unencrypted connectionunsetyes
SMTP_SECURITYstarttls (587), tls (465) or none (25)starttlsyes
SMTP_AUTHauto, plain, login, cram-md5 or noneautoyes
SMTP_EHLO_NAMEEHLO name presented to the relaythe sender domainyes
SMTP_TLS_INSECURE_SKIP_VERIFYSkips certificate verification. Only for a relay with a private certificate authorityfalseyes
EMAIL_BRAND_NAME and the other EMAIL_BRAND_* valuesName, legal entity, address and links in the transactional footerWarmbly's ownyes
NOTIFICATION_EMAIL_DAILY_CAPNotification emails per user per day. 0 means uncapped25yes
NOTIFICATION_PUSH_WINDOWHow long a notification waits before it is also pushed5hyes

log is a real transport, not a broken one: it writes every message to the backend log and delivers nothing. It exists so a fresh install can complete its first sign-in with no relay. What it costs you is password resets, invitation delivery and digests, all of which have a workaround described on accounts and access.

Read a code out of the log:

docker compose -p warmbly logs backend | grep -B2 -A12 "MAIL_TRANSPORT=log"

The consumer only warns

The backend refuses to start without EMAIL_ADDRESS and EMAIL_NAME. The consumer logs a warning and silently disables all notification and digest email, so an instance can look healthy while sending nothing. Set both on every process.

Encryption

VariableWhat it doesDefaultRestart needed
KMS_PROVIDERlocal (AES master key below) or aws (AWS KMS)local under compose, aws for a bare binaryyes
KMS_LOCAL_MASTER_KEYbase64, exactly 32 bytes. The root of trust for every per-organization data keypublished default under composeyes
KMS_LOCAL_MASTER_KEY_FILEPath to a file holding that key instead. Mutually exclusive with the inline valueunsetyes
KMS_AWS_KEY_IDKey id or alias when KMS_PROVIDER=awsunsetyes
CREDENTIALS_ENCRYPTION_KEYexactly 64 hex characters. Seals mailbox SMTP and IMAP passwords at restpublished default under composeyes
ENCRYPTED_KEYS_PROVIDERpostgres for backend and consumer, http for workersthe caller's fallback, so set it explicitlyyes
ENCRYPTED_KEYS_BACKEND_URLWhere a worker reaches the backend's key endpointunsetyes
ENCRYPTED_KEYS_WORKER_TOKENThe worker's copy of INTERNAL_API_TOKENunsetyes

An empty CREDENTIALS_ENCRYPTION_KEY does not fail at boot. It disables sealing, so mailbox passwords are stored unsealed. Set it before you connect a single mailbox, and back it up.

Storage

VariableWhat it doesDefaultRestart needed
BLOB_PROVIDERfilesystem or s3filesystem under compose, s3 for a bare binaryyes
BLOB_FS_ROOTDirectory for stored bodies, attachments and avatars. The backend, the consumer and every worker on the host must share it/data/blobsyes
BLOB_BUCKETBucket name when BLOB_PROVIDER=s3unsetyes
BLOB_PUBLIC_BASE_URLPublic base for the backend's /public routederivedyes
AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYCredentials for S3 or SESunsetyes
AWS_ENDPOINT_URL_S3Non-AWS S3 endpoint (MinIO, R2, B2)unsetyes
AWS_CONFIG_ENABLEDtrue reads secrets from AWS SSM or Secrets Managerfalseyes

On filesystem, a remote worker writes blobs to its own disk rather than a volume the backend can read. Use s3 with a bucket both sides reach when workers run off-host.

Event bus

VariableWhat it doesDefaultRestart needed
EVENTBUS_PROVIDERnats or kafka. Kafka needs images built with GO_TAGS=kafkanats under compose, kafka for a bare binaryyes
NATS_URLJetStream addressnats://nats:4222yes
NATS_STREAM_NAME, NATS_SUBJECT_PREFIXStream and subject namingwarmblyyes
KAFKA_BOOTSTRAP_SERVERSBroker list when EVENTBUS_PROVIDER=kafkaunsetyes
KAFKA_SASL_USERNAME, KAFKA_SASL_PASSWORDBroker credentialsunsetyes
SCHEMA_REGISTRY_URL, SCHEMA_REGISTRY_KEY, SCHEMA_REGISTRY_SECRETRegistry for the Avro codecunsetyes
CODEC_PROVIDERjson or avrojson under compose, avro for a bare binaryyes
EVENTBUS_HANDLER_TIMEOUTHow long one handler may take before the delivery is abandoned30syes
PUBSUB_ENABLEDfalse uses the Redis bridge for realtime fanout, true uses Google Pub/Subfalseyes
GCP_PROJECT_IDProject when PUBSUB_ENABLED=trueunsetyes

CODEC_PROVIDER=json is required wherever workers run: the worker command and result envelopes carry untyped bodies Avro cannot serialize, so any other value makes every worker command fail to encode. PUBSUB_ENABLED must agree across backend, consumer and realtime.

The tracking topic is read by two languages

KAFKA_TRACKING_TOPIC is read by the Rust publisher and the Go subscriber. Override it in one place only and opens and clicks stop being consumed, with no error anywhere.

Database

VariableWhat it doesDefaultRestart needed
PRIMARY_DBPostgreSQL connection string. Carries inline credentials, so it is never returned by any APIthe compose postgresyes
DATABASE_URLThe realtime service's own name for the same databasethe compose postgresyes
DATABASE_POOL_SIZEMaximum pooled connections for the realtime service only. The Go services use the driver default and do not read it10yes
DATABASE_SSLWhether the realtime service connects to Postgres over TLStrue, and false under composeyes

Migrations are embedded in the backend binary and applied on boot. There is no separate migration step, and a standalone /app/migrate binary ships in the image for the cases where you want one.

Cache

VariableWhat it doesDefaultRestart needed
REDISRedis connection string. Carries inline credentials, so it is never returned by any APIthe compose redisyes
REDIS_URLThe realtime service's own name for the same instancethe compose redisyes

Redis holds rate limit counters, the organization key cache, the realtime bridge and the first-run setup token. Flushing it on an unclaimed instance destroys the claim link along with every pending auth session.

GeoIP

VariableWhat it doesDefaultRestart needed
GEODB_PATHPath to a GeoLite2 City databasenone, and the backend refuses to start without the variableyes

The variable must be set on the backend in every environment. The file itself is optional: a missing file at that path means sessions and audit rows are recorded without a city, and nothing else changes.

Workers

VariableWhat it doesDefaultRestart needed
WORKER_IDStable uuid for this worker. Leave unset when running scaled replicas, which share one environmentderived, then randomyes
WORKER_BIND_IPSource address to bind outbound connections to, and the seed for a derived WORKER_IDunsetyes
WORKER_PUBLIC_IPThe address the worker reports to the control planedetectedyes
WORKER_TIERfree, premium or dedicated. Tier placement is strictfreeyes
WORKER_EGRESS_KINDLabel describing the worker's egress pathunsetyes
WORKER_IMAGEImage the remote installer pulls. The built-in default does not match what CI publishes, so set itbuilt-inyes
WORKER_INSTALLER_PATHPath to the installer script the backend servesbuilt-inyes
ENCRYPTED_KEYS_BACKEND_URLBackend base the worker fetches organization keys fromunsetyes
ENCRYPTED_KEYS_WORKER_TOKENThe worker's copy of INTERNAL_API_TOKENunsetyes
MAIL_TLS_INSECURESkips certificate verification on mailbox connectionsfalseyes

An unset key URL is silent

An empty ENCRYPTED_KEYS_BACKEND_URL or ENCRYPTED_KEYS_WORKER_TOKEN lets the worker start, subscribe and never register. There is no log line. The no_worker_heartbeat check on Instance health is what surfaces it.

Workers hold no database connection by design. Everything relational they need arrives over the backend's internal HTTP API.

Mailbox connections

Needed on the backend and every worker: the backend starts the OAuth flow, and each worker refreshes the token when it expires.

VariableWhat it doesDefault
BOX_GOOGLE_CLIENT_ID, BOX_GOOGLE_CLIENT_SECRETConnect Gmail and Google Workspace mailboxes. Redirect URI is your API base plus /addresses/google/callbackunset
BOX_OUTLOOK_CLIENT_ID, BOX_OUTLOOK_CLIENT_SECRETConnect Outlook and Microsoft 365 mailboxes. Redirect URI is your API base plus /addresses/outlook/callbackunset

Plain SMTP and IMAP mailboxes need none of this. If a worker is missing these values, the mailbox connects fine and then silently stops about an hour later, when its first access token expires.

Integrations

VariableWhat it doesDefault
<PROVIDER>_OAUTH_CLIENT_ID, <PROVIDER>_OAUTH_CLIENT_SECRETOAuth clients for the CRM and messaging integrationsunset
INTEGRATIONS_OAUTH_REDIRECT_URLShared redirect URI for those flowsderived from API_PUBLIC_URL
VariableWhat it doesDefault
AI_PROVIDERopenai, openrouter, groq, ollama, anthropic or custom. Omit every AI variable to run with AI off, in which case AI endpoints return a clean 503unset
AI_API_KEYProvider key. Not needed for ollamaunset
AI_MODEL, AI_MODEL_TRIAL, AI_MODEL_PAIDModel selection, optionally split by planprovider preset
AI_BASE_URLRequired for custom. Any OpenAI compatible endpointunset
AI_FREETreats AI usage as unchargedderived
SEARCH_PROVIDER, SEARCH_API_URL, SEARCH_API_KEYWeb search for the assistant (serper or searxng)unset

An unset provider still uses a key

An empty AI_PROVIDER with a set AI_API_KEY falls back to api.openai.com, so the key goes to OpenAI. Set both or neither.

Set these on the backend and the consumer.

Tasks and billing

VariableWhat it doesDefault
TASKS_PROVIDERlocal (an in-process Postgres poller) or gcloud (Cloud Tasks)local
TASKS_LOCAL_POLL_INTERVALHow often the local poller looks for due work1s
BILLING_PROVIDERnone (every feature unlocked, no trial expiry; the dashboard reports the workspace as self-hosted rather than on a free tier and hides billing) or stripenone
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEYRequired together when BILLING_PROVIDER=stripe. The backend exits at boot if any is missingunset

Delayed sends run through the local poller, so the backend must be running for scheduled work to fire.

Observability

VariableWhat it doesDefault
SENTRY_DSNError reporting. Optional in every environment, including produnset
APNS_KEY or APNS_KEY_PATH, APNS_KEY_ID, APNS_TEAM_ID, APNS_TOPICMobile push on backend and consumer. Partial configuration disables push with a warning, never a crashunset

Tracking service

The Rust open and click service. It reads its own environment, so these have to be set on that container, and none of them appear in the admin panel.

VariableWhat it doesDefault
TRACKING_HOST, TRACKING_PORTListen address0.0.0.0, 3000
BACKEND_INTERNAL_URLWhere tracking resolves opaque /c/<id> click tickets. Required: the service exits at boot without itnone
INTERNAL_API_TOKENBearer token for that lookup. Required: the service exits at boot on an empty valuenone
TRACKING_RATE_LIMIT_PER_MINCounted pixel and click requests per source per minute. Over budget, pixels are still served but not counted, and click redirects get 429300
EVENTBUS_PROVIDERnats or kafka. Kafka needs an image built with CARGO_FEATURES=kafkanats
NATS_URL, NATS_SUBJECT_PREFIXJetStream address and subject prefix. The publish subject is <prefix>.<topic>nats://localhost:4222, warmbly
KAFKA_TRACKING_TOPICEvent topic, read by the Rust publisher and the Go subscribertracking-events
KAFKA_BOOTSTRAP_SERVERS, KAFKA_SASL_USERNAME, KAFKA_SASL_PASSWORDBroker transport when EVENTBUS_PROVIDER=kafkaunset
SCHEMA_REGISTRY_URL, SCHEMA_REGISTRY_KEY, SCHEMA_REGISTRY_SECRETRegistry for the Avro codecunset
AWS_CONFIG_ENABLEDtrue falls back to AWS SSM and Secrets Manager for any value missing from the environmentfalse
APP_ENVEnvironment label used in logsdev

Realtime service

The Elixir websocket service. Its runtime configuration is read only when the release boots in prod, which is how the shipped image runs. Like tracking, it reads its own environment and appears nowhere in the admin panel.

VariableWhat it doesDefault
JWT_SECRETMust equal the backend's AUTH_SECRET. Required: the service refuses to boot without itnone
SECRET_KEY_BASEPhoenix session signing. Requirednone
DATABASE_URLPostgres, used to validate API keys. Requirednone
REDIS_URLThe Redis bridge the backend publishes events ontoredis://localhost:6379/0
PHX_HOSTThe service's own hostnamelocalhost
PORTListen port4000
CHECK_ORIGINtrue accepts a websocket upgrade only from PHX_HOSTfalse
PUBSUB_ENABLEDtrue swaps the Redis bridge for Google Pub/Subfalse
GCP_PROJECT_IDRequired when PUBSUB_ENABLED=true; the service refuses to boot without itunset
MAX_CONNECTIONS_PER_USERConcurrent sockets one account may hold. The caller's plan limit applies too, whichever is lower10
MAX_CONNECTIONS_PER_IPConcurrent sockets from one address50
MAX_CONNECTIONS_GLOBALConcurrent sockets on this node100000
RATE_LIMIT_WS_MESSAGEWebsocket messages per minute120
RATE_LIMIT_WS_JOINChannel joins per minute30
RATE_LIMIT_WS_EVENTClient events per minute, which is what bounds presence updates60
SENTRY_DSNError reporting. An empty string is treated as unset on purpose, because the library rejects "" hard enough to take the node downunset

CHECK_ORIGIN is false by default

The shipped default accepts a websocket upgrade from any origin. A token is still required to join a channel, so an attacker needs a valid JWT either way, but on a deployment reachable from the internet set PHX_HOST to the public websocket hostname and CHECK_ORIGIN=true so only your own dashboard can open a socket.

Settings stored in the database

These are the only settings a browser can change, and no environment variable owns any of them. They live in the admin panel under Instance > Instance settings (/configuration/settings). Reads are cached for 30 seconds in each process, so a change takes effect everywhere within that window.

SettingTypeDefaultWhat it does
invitations.ttl_hoursinteger, 1 to 720168How long a new invitation stays valid. Read when the invitation row is written, so it applies to invitations created after the change, not to existing ones
invitations.links_enabledbooleantrueWhether the copyable invitation link is returned at all. Off makes GET /organization/invitations/:id/link return 404 with an explanation, so an invitation can only arrive by mail
access.allow_invited_signupbooleantrueWhether holding a live invitation lets someone create their own account under invite_only. Off means an administrator creates every account with warmblyctl user create

Changing them is audited, and every value is validated and clamped server side on write as well as on read, so a row written by an older version still resolves.

Variables that do not do what their name suggests

VariableWhat actually happens
KAFKA_CLUSTERNothing. A loader exists but no caller does. Remove it
SENTRY_DSN_APINothing, for the same reason. Superseded by SENTRY_DSN
PROVISIONING_DRY_RUNIt is read, but it cannot be turned off. No real installer adapter is wired yet, so false logs a line and is forced back to dry-run rather than creating servers nothing could finish provisioning. PROVISIONING_RUNNER_ENABLED=false stops the runner entirely
CAPTCHA_PROVIDERRead, but derived when unset rather than defaulting to a constant. See captcha

See also

On this page