# 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) # The webmail server checks every sign-in against this URL itself, so it must # resolve and be trusted from here (e.g. inside the container), not only from # the browser. If the mail server uses a self-signed or private-CA certificate, # point NODE_EXTRA_CA_CERTS at that certificate or its CA; otherwise calendar # subscriptions and other server-side features fail with "Not authenticated". JMAP_SERVER_URL=https://your-jmap-server.com # NODE_EXTRA_CA_CERTS=/certs/mail-ca.pem # 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. An "oauth" block with both "clientId" and # "buttonLabel" gives that server its own sign-in button on the login page, # even while OAuth is off globally. "endSessionUrl" in the "oauth" block is # that server's OAUTH_END_SESSION_URL (see below). # 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 endpoints on other hosts to resolve to private (RFC-1918 / # loopback) addresses. Off by default as an SSRF guard. Endpoints on the # issuer's own host are always allowed, so split-DNS deployments where the # issuer's public hostname resolves to an internal IP work without this. Enable # only when the IdP advertises endpoints on a different internal host. # OAUTH_ALLOW_PRIVATE_ENDPOINTS=true # Signing out of an SSO account also signs out of the identity provider, # through the end_session_endpoint it advertises or OAUTH_END_SESSION_URL # (RP-initiated logout). Set to false when the provider is shared with apps # that should stay signed in. # OAUTH_END_SESSION=false # The provider's logout URL, for providers whose discovery document has no # end_session_endpoint (Stalwart's built-in provider, for example). When set, # it is used instead of the discovered endpoint and gets the same parameters: # client_id, id_token_hint and post_logout_redirect_uri. Must be https. # Applies to OAUTH_ISSUER_URL's provider only; JMAP_SERVERS entries set their # own "endSessionUrl" in their "oauth" block. # OAUTH_END_SESSION_URL=https://your-idp.example.com/logout # Where the identity provider sends the browser after signing the user out. # Register it with the provider as a post-logout redirect URI first, or it # will refuse the logout. Unset: the provider shows its own signed-out page. # OAUTH_POST_LOGOUT_REDIRECT_URI=https://mail.example.com/en/login # Replace the scopes requested at authorization. Space-separated. Leave unset # to use the defaults: openid email profile. offline_access is not among them; # add it if your IdP (e.g. Authelia) only issues refresh tokens for it, # otherwise sessions end when the access token expires. # 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. Left unset, # one is generated on first start and kept in the admin state dir # (data/admin-state), so keep that dir on a persistent volume. # 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 # # Optional existing PEM PKCS#8 Ed25519 private key used to sign managed plugin # bundles. Bulwark reads this exact path without modifying it. Use a runtime # secret mount such as a systemd credential, Docker secret, or Kubernetes Secret. # When unset, Bulwark loads or creates ADMIN_CONFIG_DIR/plugin-signing.key. # PLUGIN_SIGNING_KEY_FILE=/run/secrets/plugin-signing-key # # 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 # Stalwart version to report in heartbeats. Stalwart does not publish its # version, so the heartbeat leaves it empty unless it is set here. # STALWART_VERSION=0.16.23 # ============================================================================= # 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 # Where users get the source code of this build, linked from Settings > # About. Bulwark is AGPL-3.0-only: if you run a modified build, set this to # the source of exactly what you deploy (your fork, at the deployed commit). # Unset links to the upstream repository. Only http(s) URLs are used. # SOURCE_CODE_URL=https://git.example.com/acme/webmail/tree/v1.2.3 # 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 # # Offer "Sign in with an access token" (default false). For JMAP servers # that authenticate API tokens as Bearer tokens instead of passwords, such # as Fastmail (JMAP_SERVER_URL=https://api.fastmail.com). # LOGIN_SHOW_TOKEN_LOGIN=true # # Show the "Remember me" box (default false). Without it every sign-in is # remembered, provided SESSION_SECRET is set. # LOGIN_SHOW_REMEMBER_ME=true # # 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 # What a Stalwart admin account grants in the Bulwark admin dashboard. # auto - Stalwart admins see the admin shield and are signed into # /admin without the Bulwark admin password. Default. # password - Stalwart admins see the shield, but must enter the Bulwark # admin password. # off - Stalwart admin status is ignored: no shield, and /admin is # reachable only via /admin/login with the admin password. # "password" and "off" need ADMIN_PASSWORD (or a wizard-set password), # otherwise the dashboard would have no way in. Also editable in the admin # dashboard under Authentication. # STALWART_ADMIN_ACCESS=auto # 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. # Without any reverse proxy the header is whatever the client sends, so # per-IP limits and audit-log addresses can be forged; admin sign-in then # still stops at 50 attempts per 15 minutes across all clients. Put the # app behind a proxy that appends X-Forwarded-For. # 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 # Which other sites may frame the app, as a CSP frame-ancestors value. By # default none may. Space-separate multiple origins. 'self' is always added, # since the app frames its own PDF previews. # 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. # # The master password never leaves the server: each handoff uses it once to # create an app password on the target mailbox, valid for 8 hours and # revoked on sign-out. The target account's role therefore needs the # app-password permissions (Stalwart's default user role has them). # # 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