# Bulwark Webmail - Production Configuration # Copy this file to .env.local and fill in your values. # For development with the built-in mock server, see .env.dev.example instead. # ============================================================================= # JMAP Server (required) # ============================================================================= # App name displayed in the UI, browser tab title, and PWA manifest. APP_NAME=Bulwark Webmail # URL of your JMAP-compatible mail server (required unless ALLOW_CUSTOM_JMAP_ENDPOINT is set) JMAP_SERVER_URL=https://your-jmap-server.com # Allow users to specify a custom JMAP server URL on the login form. # When enabled, a "JMAP Server" field appears on the login page. # Users can connect to any JMAP-compatible server. # NOTE: External JMAP servers must include this domain in their CORS # Access-Control-Allow-Origin header, or browser requests will be blocked. # ALLOW_CUSTOM_JMAP_ENDPOINT=true # Offer several JMAP servers on the login form. JSON array; each entry needs # id, label, and url. "domains" and a per-server "oauth" block are optional. # Prefer configuring this from the admin dashboard - the env form exists for # stateless deployments. # JMAP_SERVERS=[{"id":"eu","label":"Europe","url":"https://eu.example.com","domains":["example.com"]},{"id":"us","label":"US","url":"https://us.example.com","oauth":{"clientId":"webmail-us"}}] # Pick the server automatically from the domain of the address the user types, # matching against each entry's "domains" list. Default: false. # JMAP_SERVER_AUTO_PICK_BY_DOMAIN=true # ============================================================================= # Stalwart Mail Server Integration # ============================================================================= # Enable Stalwart-specific features (password change, sieve filters, etc.) # Set to "false" to disable if using a non-Stalwart JMAP server. # STALWART_FEATURES=true # ============================================================================= # OAuth / OpenID Connect (optional) # ============================================================================= # Set to "true" to use OAuth instead of basic JMAP authentication # OAUTH_ENABLED=true # Set to "true" to only allow OAuth login (hides username/password form) # Requires OAUTH_ENABLED=true # OAUTH_ONLY=true # OAuth client ID registered with your identity provider # OAUTH_CLIENT_ID=your-client-id # OAuth client secret (server-side only, never exposed to the browser) # OAUTH_CLIENT_SECRET=your-client-secret # Alternatively, you can specify the path to a file containing the OAuth client secret. # OAUTH_CLIENT_SECRET_FILE=/oauth-client-secret # OpenID Connect issuer URL for discovery # OAUTH_ISSUER_URL=https://your-idp.example.com # Overrides only the user-facing authorize endpoint (e.g. a per-brand login # host). Discovery, token exchange and refresh keep using OAUTH_ISSUER_URL. # Leave unset to use the authorization_endpoint from discovery. # OAUTH_AUTHORIZE_URL=https://login.your-brand.example.com/application/o/authorize/ # Allow OAuth discovery to resolve to private (RFC-1918 / loopback) addresses. # Off by default as an SSRF guard. Enable for split-DNS deployments where the # OAuth issuer's public hostname resolves to an internal IP from this server. # OAUTH_ALLOW_PRIVATE_ENDPOINTS=true # Replace the scopes requested at authorization. Space-separated. Leave unset # to use the defaults the client already asks for. # OAUTH_SCOPES=openid email profile offline_access # Append scopes instead of replacing them. Use this when your IdP needs one # extra scope and you don't want to restate the defaults. # OAUTH_EXTRA_SCOPES=groups # Send the user straight to the identity provider, skipping the login form. # Intended for embedded deployments where the parent app already authenticated # them. Default: false. # AUTO_SSO_ENABLED=true # ============================================================================= # Session & Security # ============================================================================= # Secret key for encrypting "Remember me" sessions and settings sync data. # Required for both "Remember me" and settings sync features. # Generate with: openssl rand -base64 32 # SESSION_SECRET=your-secret-key-here # Alternatively, you can specify the path to a file containing the session secret. # SESSION_SECRET_FILE=/session-secret # ============================================================================= # Settings Sync # ============================================================================= # Enable server-side settings persistence (requires SESSION_SECRET). # When enabled, user settings are encrypted and stored on the server, # allowing them to sync across browsers and devices. # SETTINGS_SYNC_ENABLED=true # Directory for storing encrypted settings files (default: ./data/settings). # For Docker, the working directory is /app, so the default resolves to # /app/data/settings - mount a persistent volume there (see docker-compose.yml). # SETTINGS_DATA_DIR=./data/settings # ============================================================================= # Admin Dashboard Data # ============================================================================= # Admin data is split across two directories so the config volume can be # mounted read-only after the setup wizard completes (see issue #226). # # Config dir - operator-authored state. Holds config.json, policy.json, # admin.json (passwordHash only), plugin-config/, plugins/, themes/, and # branding uploads. Safe to mount read-only after setup. # Default: ./data/admin (or ADMIN_DATA_DIR if that legacy variable is set) # ADMIN_CONFIG_DIR=./data/admin # # State dir - runtime mutations. Holds admin-state.json (login timestamps), # audit.log, and the bootstrap setup token. Always read-write. # Default: ./data/admin-state (or ADMIN_DATA_DIR/state when ADMIN_DATA_DIR # is set, for back-compat with single-volume installs) # ADMIN_STATE_DIR=./data/admin-state # # Set to "true" to enforce read-only mode at the application layer (cleaner # error than a mid-request EROFS). Pair with `:ro` on the config-volume mount. # ADMIN_CONFIG_READONLY=true # # Legacy: a single dir containing both config and state. Honoured if neither # of the split variables is set. New installs should use the split vars. # ADMIN_DATA_DIR=./data/admin # ============================================================================= # Anonymous Telemetry # ============================================================================= # Anonymous instance telemetry is OPT-IN and disabled by default. Enabling it # helps us understand how Bulwark is used so we can make the product better. # Heartbeats contain no PII: version, platform, bucketed account counts, and # feature toggles only - never email addresses, hostnames, or IPs. See # https://bulwarkmail.org/docs/legal/privacy/telemetry for the full schema. # # Enable telemetry (also toggleable in the admin UI): # BULWARK_TELEMETRY=on # # Setting this (on or off) locks the choice and disables the admin UI toggle. # Directory for telemetry state: instance id, consent, login HMACs # (default: ./data/telemetry). For Docker, the default resolves to # /app/data/telemetry - mount a persistent volume there (see docker-compose.yml) # so the instance id and consent choice survive upgrades. # TELEMETRY_DATA_DIR=./data/telemetry # Legacy kill switch, honoured only when BULWARK_TELEMETRY is unset. # BULWARK_TELEMETRY_DISABLED=1 # Let heartbeats reach a private/loopback address. Off by default as an SSRF # guard; only useful when running a collector locally during development. # BULWARK_TELEMETRY_ALLOW_PRIVATE=1 # Report a fixed Stalwart version instead of probing the JMAP server's Server # header. Useful when a proxy strips that header. # STALWART_VERSION=0.16.0 # ============================================================================= # Server Listen Address # ============================================================================= # Hostname the server binds to (default: 0.0.0.0) # Set to "::" for dual-stack # HOSTNAME=0.0.0.0 # Port the server listens on (default: 3000) # PORT=3000 # ============================================================================= # Logging # ============================================================================= # Log format: "text" (colored, human-readable) or "json" (structured, for log aggregation) # LOG_FORMAT=text # Log level: "error", "warn", "info", or "debug" # LOG_LEVEL=info # ============================================================================= # Branding (all optional) # ============================================================================= # --------------------------------------------------------------------------- # App identity # --------------------------------------------------------------------------- # Short name for the app, used in contexts where space is limited # (e.g. home screen label on mobile). Defaults to APP_NAME if not set. # APP_SHORT_NAME=Bulwark # Description shown in the PWA manifest (displayed by the OS during install). # Defaults to a generic Bulwark description if not set. # APP_DESCRIPTION=Your personal webmail # --------------------------------------------------------------------------- # Icons & favicon # --------------------------------------------------------------------------- # Custom favicon shown in the browser tab. # Supported formats: SVG (recommended), PNG, ICO. # Can be an absolute URL (https://...) or a path relative to the public/ directory. # Defaults to the Bulwark favicon if not set. # FAVICON_URL=/branding/my-favicon.svg # Source image used to auto-generate PWA icons (192×192 and 512×512 PNG). # Supported formats: SVG (recommended for best quality) or PNG (≥512×512px recommended). # Can be an absolute URL (https://...) or a path relative to the public/ directory. # Falls back to FAVICON_URL if not set, and to the default Bulwark icons if neither is set. # PWA_ICON_URL=/branding/my-icon.svg # --------------------------------------------------------------------------- # PWA appearance # --------------------------------------------------------------------------- # Color applied to the browser UI chrome when the app is installed as a PWA # (address bar, status bar on Android). Default: #ffffff # PWA_THEME_COLOR=#3b82f6 # Background color shown on the PWA splash screen while the app is loading. # Should match your app's main background color. Default: #ffffff # PWA_BACKGROUND_COLOR=#ffffff # Screenshots shown in the browser's install prompt. Absolute URLs or paths # relative to public/. Both are optional; per-domain overrides are available # through DOMAIN_BRANDING. # PWA_SCREENSHOT_MOBILE_URL=/branding/screenshot-mobile.png # PWA_SCREENSHOT_DESKTOP_URL=/branding/screenshot-desktop.png # --------------------------------------------------------------------------- # Logos # --------------------------------------------------------------------------- # Logos shown in the sidebar (main app, after login). # Supported formats: SVG (recommended), PNG, WebP. # Recommended size: min 24×24px, max 128×128px. # Can be absolute URLs or paths relative to the public/ directory. # If not set, no logo is shown in the sidebar. # APP_LOGO_LIGHT_URL=/branding/my-logo-color.svg # APP_LOGO_DARK_URL=/branding/my-logo-white.svg # Logos shown on the login page. # Supported formats: SVG (recommended), PNG, WebP. # Recommended size: min 32×32px, max 512×512px. # Can be absolute URLs or paths relative to the public/ directory. # Light mode logo (shown on light backgrounds). Defaults to the Bulwark logo. LOGIN_LOGO_LIGHT_URL=/branding/Bulwark_Logo_Color.svg # Dark mode logo (shown on dark backgrounds). Defaults to the Bulwark white logo. LOGIN_LOGO_DARK_URL=/branding/Bulwark_Logo_Color.svg # --------------------------------------------------------------------------- # Login page # --------------------------------------------------------------------------- # Company name shown above the version number on the login page. LOGIN_COMPANY_NAME=Bulwark Webmail # URL for the imprint / legal notice link on the login page. # LOGIN_IMPRINT_URL=https://example.com/imprint # URL for the privacy policy link on the login page. # LOGIN_PRIVACY_POLICY_URL=https://example.com/privacy # URL for the company website link on the login page. LOGIN_WEBSITE_URL=https://bulwarkmail.org # Cap the login logo's rendered size. Any CSS length ("120px", "8rem"). # Unset means the logo renders at its natural size. # LOGIN_LOGO_MAX_HEIGHT=96px # LOGIN_LOGO_MAX_WIDTH=320px # Hide parts of the login page. All default to true. # Turn the heading and subtitle off when the logo already reads as the brand. # LOGIN_SHOW_HEADING=false # LOGIN_SHOW_SUBTITLE=false # # Hide the optional TOTP field. A server that requires TOTP (totp_required) # still shows it regardless of this setting. # LOGIN_SHOW_TOTP=false # # Hide the version number, so it isn't disclosed to unauthenticated visitors. # LOGIN_SHOW_VERSION=false # --------------------------------------------------------------------------- # Per-domain branding overrides (optional) # --------------------------------------------------------------------------- # # When you serve the webmail on multiple hostnames, each hostname can override # a subset of branding fields. Unset fields fall back to the global values # above. Match is on the request's Host (or X-Forwarded-Host) header. # # Use the leftmost label "*." to match any subdomain (e.g. "*.example.com" # matches mail.example.com and any deeper subdomain, but NOT example.com). # Exact matches always win over wildcards; the longest wildcard suffix wins # among multiple wildcard matches. # # Overridable keys: appName, appShortName, appDescription, faviconUrl, # pwaIconUrl, pwaThemeColor, pwaBackgroundColor, appLogoLightUrl, # appLogoDarkUrl, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, # loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl. # # Prefer setting this from the admin dashboard (PATCH /api/admin/config). # The env-var form is provided for stateless deployments. # # DOMAIN_BRANDING=[{"host":"maildomain1.com","loginCompanyName":"Company One","loginLogoLightUrl":"/branding/one-color.svg","loginLogoDarkUrl":"/branding/one-white.svg","loginWebsiteUrl":"https://one.example"},{"host":"maildomain2.com","loginCompanyName":"Company Two","faviconUrl":"/branding/two-favicon.svg"},{"host":"*.intranet.example.com","loginCompanyName":"Internal"}] # ============================================================================= # Extension Directory / Marketplace # ============================================================================= # URL of the BulwarkMail extension directory for the admin marketplace. # Defaults to https://extensions.bulwarkmail.org. Override only if you run # your own directory (e.g. http://localhost:3001 for local development). # EXTENSION_DIRECTORY_URL=https://extensions.bulwarkmail.org # ============================================================================= # Admin Dashboard Access # ============================================================================= # Bootstrap password for the admin dashboard. Read only when admin.json does # not already exist; the app hashes it, writes admin.json, and logs a warning # telling you to remove this variable. Without it (and without the setup # wizard) the admin dashboard stays disabled. # Accepts a plaintext password or an existing hash. # ADMIN_PASSWORD=change-me # Admin session lifetime in seconds. Default: 3600 (1 hour). # ADMIN_SESSION_TTL=3600 # How many trusted reverse proxies sit in front of the app. The client IP is # taken that many entries from the right of X-Forwarded-For, so an attacker # can't spoof it by prepending values. Default: 1. # TRUSTED_PROXY_DEPTH=2 # Allow search engines to index the app (robots.txt / noindex). Default: false. # SEARCH_ENGINE_INDEXING=true # ============================================================================= # Cookies, Embedding & Reverse Proxies # ============================================================================= # SameSite attribute for session cookies: lax (default), strict, or none. # Embedding the app cross-origin in an iframe requires "none". # COOKIE_SAME_SITE=none # Force the Secure flag on cookies. Defaults to on when NODE_ENV=production or # COOKIE_SAME_SITE=none. Set to false only for local HTTP development. # COOKIE_SECURE=false # Who may frame the app, as a CSP frame-ancestors value. Defaults to 'none', # which blocks all framing. Space-separate multiple origins. # ALLOWED_FRAME_ANCESTORS=https://portal.example.com # Origin of the parent page when embedded, used for postMessage handshakes. # NEXT_PUBLIC_PARENT_ORIGIN=https://portal.example.com # ============================================================================= # Update Check # ============================================================================= # The app periodically checks for new releases and shows a notice. Set to # "off" (or false/0/no) to disable the check entirely. # BULWARK_UPDATE_CHECK=off # Override the endpoint it checks. Takes priority over the on-disk state file. # An explicit empty value also disables the check. # BULWARK_UPDATE_CHECK_URL=https://updates.example.com/bulwark.json # Where the check stores its state. Default: ./data/version-check # VERSION_CHECK_DATA_DIR=./data/version-check # ============================================================================= # Translation Proxy (optional) # ============================================================================= # /api/translate defaults to the public MyMemory API, which needs no setup. # Point it at a LibreTranslate instance instead to keep message text on # infrastructure you control. LibreTranslate also auto-detects the source # language natively. # LIBRETRANSLATE_URL=https://libretranslate.example.com # LIBRETRANSLATE_API_KEY= # ============================================================================= # Web Push # ============================================================================= # Push notifications go through a hosted relay so self-hosters don't need # their own VAPID keys and Firebase project. Point this at your own relay to # avoid the default. Build-time variable. # Default: https://notifications.relay.bulwarkmail.org # NEXT_PUBLIC_PUSH_RELAY_URL=https://push.example.com # ============================================================================= # Demo Mode # ============================================================================= # Serve fixture data instead of talking to a mail server. Default: false. # DEMO_MODE=true # ============================================================================= # Stalwart Impersonation (advanced) # ============================================================================= # Lets a trusted platform mint a JWT that logs a user in without their # password, using a Stalwart master account. Intended for embedded # deployments where an outer platform already authenticated the user. # # SECURITY: this grants sign-in as any mailbox on the server. The endpoint # returns 404 unless all three required variables below are set, so leaving # them unset keeps the feature fully off. Treat the secret and the master # password as you would a root credential. # # BULWARK_JWT_AUTH_SECRET= # required, >= 32 characters # BULWARK_STALWART_MASTER_USER= # required, e.g. master@example.com # BULWARK_STALWART_MASTER_PASSWORD= # required # BULWARK_JWT_AUTH_ISSUER= # optional, default "platform-api/webmail" # ============================================================================= # Internationalization # ============================================================================= # These are build-time variables - to change them with the published Docker # image, rebuild it with --build-arg (see README "Default UI locale"). # # Fallback UI locale used when the visitor's Accept-Language header does not # match any supported locale. Defaults to "en". # Supported: ar, ca, cs, da, de, en, es, fa, fr, he, hu, it, ja, ko, lv, nl, pl, # pt, ro, ru, sk, tr, uk, zh # An unsupported value falls back to "en". # NEXT_PUBLIC_DEFAULT_LOCALE=tr # Locale prefix mode for URLs. Recommended "always" when proxying under a # subpath (NEXT_PUBLIC_BASE_PATH) to avoid next-intl rewrite loops. # Values: never (default) | always | as-needed # NEXT_PUBLIC_LOCALE_PREFIX=always # ============================================================================= # Legacy Build-time Variables (still supported as fallback) # ============================================================================= # These are baked into the bundle at build time. The runtime variables above # take priority when both are set. # # NEXT_PUBLIC_APP_NAME=Bulwark Webmail # NEXT_PUBLIC_JMAP_SERVER_URL=https://your-jmap-server.com