# ============================================ # Warmbly environment reference # ============================================ # You do not need this file to start. `make up` boots the whole platform with # no .env at all and prints a link to claim the instance. Copy this file when # you are ready to run something other people can reach. # # Defaults are no-cloud: with just the secrets in the "Required" block set, # Warmbly runs with no AWS, GCP, Stripe, or Kafka. Each subsystem is a provider # switch, flip one to opt into a cloud service. # # Config priority: env var first, then AWS SSM/Secrets Manager (only when # AWS_CONFIG_ENABLED=true). # ============================================ # === Required === # dev | prod. dev allows the published default secrets, so set prod for # anything other people can reach. prod needs no cloud account: error reporting # and GeoIP are used when configured and skipped with a logged note when not. APP_ENV=prod # cloud | self_hosted. Picks the auth defaults (login code, signup lockdown, # email verification); each stays individually overridable below. DEPLOYMENT_MODE=self_hosted # JWT / session signing. Min 32 chars. MUST match the realtime service JWT_SECRET. AUTH_SECRET=change-me-min-32-characters-long # 64 hex chars (32 bytes). Seals mailbox SMTP/IMAP credentials at rest. # BACK IT UP — losing it makes connected mailboxes unrecoverable. `openssl rand -hex 32` CREDENTIALS_ENCRYPTION_KEY= # Shared token for the backend internal API (workers + tracking use it). INTERNAL_API_TOKEN=change-me-internal-token PRIMARY_DB=postgres://warmbly:warmbly@localhost:5432/warmbly_dev?sslmode=disable REDIS=redis://localhost:6379 # === Provider switches (no-cloud defaults) === AWS_CONFIG_ENABLED=false # true => read secrets from AWS SSM/Secrets Manager # Event bus. nats (default): 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://localhost:4222 # NATS_STREAM_NAME=warmbly # NATS_SUBJECT_PREFIX=warmbly # Serialization. json is required with NATS and wherever workers run. avro is # only for a Kafka + Schema Registry deployment. CODEC_PROVIDER=json # Encryption root key. local (default): AES master key below. aws: AWS KMS. KMS_PROVIDER=local # base64 32 bytes. BACK IT UP — losing it is unrecoverable. `make gen-key` KMS_LOCAL_MASTER_KEY= # KMS_LOCAL_MASTER_KEY_FILE=/run/secrets/kms_master_key # alternative to the inline key # KMS_AWS_KEY_ID=alias/warmbly # when KMS_PROVIDER=aws # Blob storage. filesystem (default): a local dir. s3: any S3-compatible store. BLOB_PROVIDER=filesystem BLOB_FS_ROOT=/data/blobs # shared between backend + workers (same host / shared volume) # Public URL base for avatars/logos served by the backend's /public route. BLOB_PUBLIC_BASE_URL=http://localhost:8080/public # For BLOB_PROVIDER=s3 (AWS / MinIO / R2 / B2): # BLOB_BUCKET=warmbly # AWS_ENDPOINT_URL_S3=http://minio:9000 # non-AWS endpoint # AWS_REGION=us-east-1 # AWS_ACCESS_KEY_ID= # AWS_SECRET_ACCESS_KEY= # Delayed tasks (campaign ticks, scheduled sends). local (default): an in-process # Postgres poller — no external service. gcloud: GCP Cloud Tasks. TASKS_PROVIDER=local # TASKS_LOCAL_POLL_INTERVAL=1s # When TASKS_PROVIDER=gcloud, also set: # CLOUD_TASKS_QUEUE_NAME=projects/

/locations//queues/ # CLOUD_TASKS_WEBHOOK_URL=https:///webhook/email # GOOGLE_APPLICATION_CREDENTIALS_JSON= # Billing. none (default): no Stripe, every feature unlocked, no trial expiry. # stripe: wire the Stripe integration. All three keys below are then required — # the backend exits at boot if any one of them is missing. BILLING_PROVIDER=none # STRIPE_SECRET_KEY= # STRIPE_WEBHOOK_SECRET= # STRIPE_PUBLISHABLE_KEY= # Captcha. Auto-off when TURNSTILE_SECRET is unset; set CAPTCHA_PROVIDER=turnstile # + TURNSTILE_SECRET to require Cloudflare Turnstile on auth endpoints. # CAPTCHA_PROVIDER=none # TURNSTILE_SECRET= # === Backend API === API_HOST=0.0.0.0:8080 # binds all interfaces already GIN_MODE=release # debug | release # With the shipped docker-compose.yml, set PUBLIC_HOST to your LAN IP or domain # and APP_URL / CORS_ALLOW_ORIGINS / WEBSOCKET_URL / TRACKING_DOMAIN / the VITE_* # frontend URLs all derive from it. Set them explicitly if you're not using that # compose (e.g. behind a reverse proxy on https://your-domain). # PUBLIC_HOST=192.168.1.50 # API_PUBLIC_URL is the compose input for the API base the frontends and blob # URLs use (falls back to http://:8080); set it explicitly behind # a reverse proxy. ENV_LABEL labels the admin panel (compose: WARMBLY_ENV_LABEL). # API_PUBLIC_URL=https://api.example.com # ENV_LABEL=production APP_URL=http://localhost:5173 CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost:5174 WEBSOCKET_URL=ws://localhost:4000/socket/websocket ENCRYPTED_KEYS_PROVIDER=postgres # backend/consumer: postgres; workers: http (below) GEODB_PATH=/app/data/GeoLite2-City.mmdb # optional everywhere; missing = no location labels # === Mailbox connections === # Gmail mailboxes need YOUR Google Cloud OAuth client (a "Web application" client) # with authorized redirect URI /addresses/google/callback. Set on the # backend AND every worker. Leave unset to connect mailboxes only via SMTP/IMAP. BOX_GOOGLE_CLIENT_ID= BOX_GOOGLE_CLIENT_SECRET= # Microsoft 365 / Outlook OAuth client (redirect /addresses/outlook/callback): BOX_OUTLOOK_CLIENT_ID= BOX_OUTLOOK_CLIENT_SECRET= # === Social sign-in (all optional; email+password / passkeys work standalone) === # GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI are the LOGIN client, # separate from the BOX_GOOGLE_* mailbox client above. # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= # GOOGLE_REDIRECT_URI= # GOOGLE_IOS_CLIENT_ID= # APPLE_APP_ID= # APPLE_TEAM_ID= # APPLE_KEY_ID= # APPLE_KEY_SECRET= # Passkeys (WebAuthn): derived from APP_URL when unset. Changing the RP ID # invalidates enrolled passkeys, so keep it stable per deployment. # WEBAUTHN_RP_ID=app.example.com # WEBAUTHN_RP_ORIGINS=https://app.example.com # === Worker (each worker process) === # Workers hold no relational DB; they reach DEKs over the backend internal API. # ENCRYPTED_KEYS_PROVIDER=http # ENCRYPTED_KEYS_BACKEND_URL=http://backend:8080 # ENCRYPTED_KEYS_WORKER_TOKEN= # WORKER_ID= # stable identity; otherwise derived from hostname # === Platform email (login codes, resets, invitations, digests) === # # MAIL_TRANSPORT is the primary switch: smtp | log | ses. # # log — writes every message to the backend logs and delivers nothing. The # default for a fresh install so first login works with no relay. # smtp — a real submission relay. Set SMTP_HOST plus the credentials below. # ses — AWS SES; needs AWS credentials and a verified identity. MAIL_TRANSPORT=log EMAIL_NAME=Warmbly EMAIL_ADDRESS=noreply@example.com TRACKING_DOMAIN=localhost:3000 # SMTP_SECURITY is starttls | tls | none. starttls is submission on 587 and the # right answer for almost every provider; tls is implicit TLS on 465; none is # cleartext and only legitimate for a sink on the same host. The port defaults # per mode, so setting either one 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 # === Auth policy === # # AUTH_LOGIN_CODE: always | new_device | off. Self-host defaults to off, so a # login never depends on outbound mail. Registration and password reset still # use emailed codes, which is what NIST SP 800-63B permits email for. # AUTH_LOGIN_CODE=off # REQUIRE_EMAIL_VERIFICATION=false # # DISABLE_REGISTRATION: true | false | invite_only. Self-host defaults to # invite_only, with a first-launch exemption so the very first signup works. # DISABLE_REGISTRATION=invite_only # DISABLE_PASSWORD_LOGIN=false # OIDC-only deployments # AUTH_IP_RATE_LIMIT=60 # unauthenticated auth requests per IP per 15 min # # CIDRs allowed to set X-Forwarded-For. Empty trusts nothing, which is correct # for a directly exposed backend. Set it when a reverse proxy sits in front, or # rate limits and audit IPs are attacker-controlled. # TRUSTED_PROXIES=10.0.0.0/8 # First owner, read only while the users table is empty. Leave unset and the # backend prints a single-use setup link to its logs on first boot. # WARMBLY_BOOTSTRAP_EMAIL=you@example.com # WARMBLY_BOOTSTRAP_PASSWORD_HASH= # argon2 PHC string; preferred # WARMBLY_BOOTSTRAP_PASSWORD= # plaintext convenience; warns at boot # WARMBLY_BOOTSTRAP_ORG=My Organization # === Single sign-on (generic OpenID Connect) === # # The only sign-in path with no dependency on outbound mail, so it is the # recommended posture for a deployment with no relay. Works with Authentik, # Keycloak, Zitadel, Pocket ID, Dex and anything else that publishes a # discovery document. RS256 ID tokens only. # OIDC_ISSUER_URL=https://id.example.com/application/o/warmbly/ # OIDC_CLIENT_ID= # OIDC_CLIENT_SECRET= # OIDC_REDIRECT_URL= # defaults to API_PUBLIC_URL/api/v1/auth/oidc/callback # OIDC_SCOPES=openid,profile,email # OIDC_ALLOWED_DOMAINS=example.com # OIDC_DEFAULT_ORG= # org every SSO user joins; without it each # # new user gets their own single-member org # OIDC_PROVIDER_NAME=Single sign-on # === 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= # === Tracking service (open/click) === TRACKING_HOST=0.0.0.0 TRACKING_PORT=3000 # Resolves opaque /c/ click tickets via the backend internal API. # BACKEND_INTERNAL_URL=http://backend:8080 TRACKING_RATE_LIMIT_PER_MIN=300 # === Realtime service (Elixir/Phoenix) === PHX_HOST=localhost PORT=4000 JWT_SECRET=change-me-min-32-characters-long # MUST equal the backend AUTH_SECRET SECRET_KEY_BASE=change-me-phoenix-secret-key-base-min-64-characters-long DATABASE_URL=postgres://warmbly:warmbly@localhost:5432/warmbly_dev?sslmode=disable REDIS_URL=redis://localhost:6379 # Realtime transport. false (default): Redis bridge, no cloud. Read identically by # backend, consumer, and realtime — never set true on one side only. PUBSUB_ENABLED=false CHECK_ORIGIN=false # When PUBSUB_ENABLED=true (Google Pub/Sub): also set GCP_PROJECT_ID + # GOOGLE_APPLICATION_CREDENTIALS_JSON on every service. # GCP_PROJECT_ID= # === AI provider (optional; omit all to run with AI features off) === # Set on the backend AND consumer. Self-host bills your provider directly, so the # credit ledger is bypassed (unlimited) when BILLING_PROVIDER=none. # AI_PROVIDER= # openai | openrouter | groq | ollama | anthropic | custom # AI_API_KEY= # AI_MODEL= # AI_BASE_URL= # required for custom # SEARCH_PROVIDER= # optional web-search tool # SEARCH_API_URL= # SEARCH_API_KEY= # === Observability === # Optional in dev; REQUIRED (fatal at boot) on every Go service when APP_ENV=prod. # SENTRY_DSN=