# ============================================================================== # Warmbly environment # ============================================================================== # Copy this file to .env next to docker-compose.yml: # # cp .env.example .env # # You do not need it to start. `make up` boots the whole platform with no .env # at all and prints a link that claims the instance. Copy this file when you are # ready to run something other people can reach. # # Everything here is read from the process environment. The environment is # authoritative: no screen in the product ever overwrites a value you set here. # The admin panel on :5174 shows the backend process's resolved value for most # of these under Instance > Configuration, with its source and whether a change # needs a restart. It does not read the realtime or tracking containers, so the # variables in those two blocks below are not on that page. # # Full reference, with defaults and restart requirements per variable: # https://docs.warmbly.com/development/configuration/ # # ------------------------------------------------------------------------------ # HOW EMPTY VALUES BEHAVE, BECAUSE IT IS NOT WHAT YOU EXPECT # # docker-compose.yml reads this file as ${VAR:-default}, and Compose treats an # EMPTY assignment exactly like a missing one. So `KMS_LOCAL_MASTER_KEY=` does # not blank the key, it substitutes the published default from # docker-compose.yml. To leave a value unset, comment the line out; to change # it, give it a real value. There is no way to force "empty" here. # ------------------------------------------------------------------------------ # CHANGE THESE FIVE BEFORE ANYONE ELSE CAN REACH THIS INSTANCE # # AUTH_SECRET INTERNAL_API_TOKEN SECRET_KEY_BASE # KMS_LOCAL_MASTER_KEY CREDENTIALS_ENCRYPTION_KEY # # docker-compose.yml ships a working default for each so a fresh clone boots. # All five defaults are published in this repository, so they protect nothing: # anyone can forge a session token or unwrap every organization key. They ship # commented out below, so this file boots as-is on those published defaults and # the backend logs a warning. Generate real values (the Secrets block has the # commands), uncomment the five lines, THEN uncomment APP_ENV=prod: with prod # set, the backend refuses to start while any of the five is still a default. # ------------------------------------------------------------------------------ # === Deployment =============================================================== # dev | prod. dev tolerates the published default secrets and turns on debug # logging. prod needs no cloud account. Uncomment this only after the five # secrets below hold real values, or the backend refuses to start. # APP_ENV=prod # cloud | self_hosted. Picks the auth defaults in the Auth policy block below; # every one stays individually overridable. DEPLOYMENT_MODE=self_hosted # true lets the backend boot on a published default secret even under # APP_ENV=prod. For a throwaway instance only. The shipped docker-compose.yml # does not forward it, so under compose it only works if you add it there. # ALLOW_INSECURE_DEFAULTS=false # debug | release. The shipped docker-compose.yml pins the backend to release, # so this line only takes effect outside compose. # GIN_MODE=release # Label shown next to the instance name in the admin panel. # ENV_LABEL=production # true lets customer webhooks point at http:// and private addresses, which # lets any workspace member make the backend reach into your network. # Development only. Not forwarded by the shipped docker-compose.yml. # WARMBLY_ALLOW_UNSAFE_WEBHOOK_URLS=false # === Secrets (CHANGE ALL FIVE) ================================================ # # Generate all five at once and append them to this file: # # cat >> .env < Members and send it yourself. # smtp a real submission relay. Set SMTP_HOST plus the credentials below. # ses AWS SES; needs AWS credentials and a verified identity. # # A bare binary with neither MAIL_TRANSPORT nor SMTP_HOST falls back to ses. MAIL_TRANSPORT=log # The backend refuses to start without these two. The consumer only WARNS and # then silently sends no notification or digest mail, so set them everywhere. EMAIL_NAME=Warmbly EMAIL_ADDRESS=noreply@example.com # SMTP_SECURITY is starttls (587) | tls (465) | none (25). The port follows the # mode, so setting either alone is enough. Credentials are never sent over an # unencrypted connection. # SMTP_HOST=smtp.example.com # SMTP_PORT=587 # SMTP_USERNAME= # SMTP_PASSWORD= # SMTP_SECURITY=starttls # SMTP_AUTH=auto # auto | plain | login | cram-md5 | none # SMTP_EHLO_NAME= # defaults to the sender domain # SMTP_TLS_INSECURE_SKIP_VERIFY=false # only for a relay with a private CA # Notification volume. # NOTIFICATION_EMAIL_DAILY_CAP=25 # per user per day; 0 means uncapped # NOTIFICATION_PUSH_WINDOW=5h # === Transactional email branding ============================================= # A self-hosted install should not send mail attributed to another company. # EMAIL_BRAND_NAME=Acme # EMAIL_BRAND_LEGAL_ENTITY=Acme Ltd # EMAIL_BRAND_COMPANY_NUMBER= # EMAIL_BRAND_PLACE_OF_REG= # EMAIL_BRAND_ADDRESS= # EMAIL_BRAND_WEBSITE_URL=https://acme.example.com # EMAIL_BRAND_SUPPORT_EMAIL=support@acme.example.com # EMAIL_BRAND_TERMS_URL= # EMAIL_BRAND_PRIVACY_URL= # === Provider switches ======================================================== # # Every provider default IN CODE is the cloud one (kafka, avro, s3, aws), so a # deployment that is not using the shipped docker-compose.yml must set all five # explicitly or the process exits at boot. # true reads secrets from AWS SSM / Secrets Manager. AWS_CONFIG_ENABLED=false # Event bus. nats: one small JetStream binary. kafka: build the images with # GO_TAGS=kafka / CARGO_FEATURES=kafka and set KAFKA_* below. EVENTBUS_PROVIDER=nats NATS_URL=nats://nats:4222 # NATS_STREAM_NAME=warmbly # NATS_SUBJECT_PREFIX=warmbly # KAFKA_BOOTSTRAP_SERVERS=broker:9092 # KAFKA_SASL_USERNAME= # KAFKA_SASL_PASSWORD= # SCHEMA_REGISTRY_URL= # SCHEMA_REGISTRY_KEY= # SCHEMA_REGISTRY_SECRET= # EVENTBUS_HANDLER_TIMEOUT=30s # Serialization. json is REQUIRED wherever workers run: worker command and # result envelopes carry untyped bodies Avro cannot serialize. CODEC_PROVIDER=json # Encryption root key provider. local: the AES master key above. aws: AWS KMS. KMS_PROVIDER=local # Instead of the inline key. Not forwarded by the shipped docker-compose.yml. # KMS_LOCAL_MASTER_KEY_FILE=/run/secrets/kms_master_key # KMS_AWS_KEY_ID=alias/warmbly # Blob storage. filesystem: a local dir shared by backend, consumer and every # worker on the host. s3: any S3-compatible store, and the right answer once # workers run off-host. BLOB_PROVIDER=filesystem BLOB_FS_ROOT=/data/blobs # Derives from API_PUBLIC_URL, and so from PUBLIC_HOST, when unset. Setting it # by hand opts it out of that. # BLOB_PUBLIC_BASE_URL=http://localhost:8080/public # BLOB_BUCKET=warmbly # AWS_ENDPOINT_URL_S3=http://minio:9000 # AWS_REGION=us-east-1 # AWS_ACCESS_KEY_ID= # AWS_SECRET_ACCESS_KEY= # Delayed tasks (campaign ticks, scheduled sends). local is an in-process # Postgres poller, so the backend must be running for scheduled work to fire. TASKS_PROVIDER=local # TASKS_LOCAL_POLL_INTERVAL=1s # Billing. none unlocks every feature and never expires a trial. BILLING_PROVIDER=none # STRIPE_SECRET_KEY= # STRIPE_WEBHOOK_SECRET= # STRIPE_PUBLISHABLE_KEY= # Where the backend and consumer read organization keys from. Workers use http. # The shipped docker-compose.yml pins it per service, so this line only takes # effect outside compose. # ENCRYPTED_KEYS_PROVIDER=postgres # === Mailbox connections ====================================================== # # Needed on the backend AND every worker: the backend starts the OAuth flow, # each worker refreshes the token. A worker missing these connects the mailbox # fine and then stops silently about an hour later. # # Plain SMTP + IMAP mailboxes need none of this. # Redirect URI: /addresses/google/callback # BOX_GOOGLE_CLIENT_ID= # BOX_GOOGLE_CLIENT_SECRET= # Redirect URI: /addresses/outlook/callback # BOX_OUTLOOK_CLIENT_ID= # BOX_OUTLOOK_CLIENT_SECRET= # === Worker (set in each worker's own environment) ============================ # # Workers hold no database. They reach organization keys over the backend's # internal API. An unset URL or token lets a worker start, subscribe and NEVER # register, with no log line. WORKER_BIND_IP, WORKER_PUBLIC_IP and # WORKER_INSTALLER_PATH are not forwarded by the shipped docker-compose.yml. # ENCRYPTED_KEYS_PROVIDER=http # ENCRYPTED_KEYS_BACKEND_URL=http://backend:8080 # ENCRYPTED_KEYS_WORKER_TOKEN= # same value as INTERNAL_API_TOKEN # # Stable identity. Set it only when you run one worker per host; scaled replicas # share an environment and would collide. # WORKER_ID= # WORKER_BIND_IP= # WORKER_PUBLIC_IP= # WORKER_TIER=free # free | premium | dedicated # WORKER_EGRESS_KIND= # MAIL_TLS_INSECURE=false # skips cert verification on mailbox connections # # Set on the BACKEND: the image the remote installer pulls. The built-in default # does not match what CI publishes. # WORKER_IMAGE=ghcr.io//warmbly/worker:prod # WORKER_INSTALLER_PATH= # === Tracking service (Rust, open/click) ====================================== # # The shipped docker-compose.yml pins the listen address, so these two only take # effect outside compose. # TRACKING_HOST=0.0.0.0 # TRACKING_PORT=3000 # Where tracking resolves opaque /c/ click tickets. Required: the service # exits at boot without it, and without INTERNAL_API_TOKEN. # BACKEND_INTERNAL_URL=http://backend:8080 # Counted pixel and click requests per source per minute. Over budget, pixels # are still served but not counted and click redirects get 429. # TRACKING_RATE_LIMIT_PER_MIN=300 # 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. # KAFKA_TRACKING_TOPIC=tracking-events # === Realtime service (Elixir/Phoenix) ======================================== # # Read only when the release boots with MIX_ENV=prod, which is how the image # runs. The shipped docker-compose.yml derives PORT, DATABASE_URL, REDIS_URL and # JWT_SECRET from the values above, so those four only take effect outside # compose. None of this block is on the admin panel's configuration page: that # registry reads the backend process, not this container. # The realtime service's own hostname. Derives from PUBLIC_HOST under compose. # PHX_HOST=localhost # PORT=4000 # MUST equal the backend's AUTH_SECRET or every websocket token is rejected and # the dashboard loads but never goes live. Compose sets it from AUTH_SECRET. # JWT_SECRET= # DATABASE_URL=postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable # DATABASE_SSL=false # REDIS_URL=redis://redis:6379 # false accepts a websocket upgrade from ANY origin, which is the shipped # default. Set true once PHX_HOST is the public websocket hostname, so only that # origin can open a socket. # CHECK_ORIGIN=false # Concurrent websocket connections. Per user is also capped by the caller's # plan, whichever is lower. # MAX_CONNECTIONS_PER_USER=10 # MAX_CONNECTIONS_PER_IP=50 # MAX_CONNECTIONS_GLOBAL=100000 # Websocket rate limits, per minute. # RATE_LIMIT_WS_MESSAGE=120 # RATE_LIMIT_WS_JOIN=30 # RATE_LIMIT_WS_EVENT=60 # Realtime transport. false uses the Redis bridge and needs no cloud. Read # identically by backend, consumer and realtime; never set true on one side. PUBSUB_ENABLED=false # GCP_PROJECT_ID= # required when PUBSUB_ENABLED=true # === AI provider (optional) =================================================== # # Omit every AI variable to run with AI off: AI endpoints return a clean 503 and # everything else works. Set these on the backend AND the consumer. # # An empty AI_PROVIDER with a set AI_API_KEY falls back to api.openai.com, which # sends your key there. Set both or neither. # AI_PROVIDER= # openai | openrouter | groq | ollama | anthropic | custom # AI_API_KEY= # AI_MODEL= # AI_MODEL_TRIAL= # AI_MODEL_PAID= # AI_BASE_URL= # required for custom # AI_FREE= # SEARCH_PROVIDER= # serper | searxng # SEARCH_API_URL= # SEARCH_API_KEY= # === Integrations (optional) ================================================== # Shared redirect URI for the CRM and messaging OAuth flows. Unset, it becomes # BACKEND_PUBLIC_URL + /integrations/oauth/callback, and http://localhost:8080 # when that is unset too. Not forwarded by the shipped docker-compose.yml. # INTEGRATIONS_OAUTH_REDIRECT_URL= # BACKEND_PUBLIC_URL=https://api.example.com # === Observability and push (optional) ======================================== # Optional in EVERY environment, including APP_ENV=prod. # SENTRY_DSN= # # Mobile push. Partial configuration disables push with a warning, never a # crash. Set on the backend and the consumer. Not forwarded by the shipped # docker-compose.yml. # APNS_KEY= # APNS_KEY_PATH= # APNS_KEY_ID= # APNS_TEAM_ID= # APNS_TOPIC=