Copy .env.example to .env and treat the resulting file as a secret. This page explains the settings that are specific or especially important to Maildun; Laravel's standard settings retain their normal behavior.
| Variable | Purpose | Production guidance |
|---|---|---|
APP_KEY |
Encrypts application secrets and signed data | Generate once, back it up securely, and do not rotate casually |
APP_PREVIOUS_KEYS |
Former APP_KEY values kept as decryption fallbacks |
Set during a rotation so encrypted workspace credentials keep working |
APP_URL |
Canonical origin for signed links and the passkey relying party | Use the public HTTPS origin |
APP_DEBUG |
Detailed exception output | Always false in production |
APP_SOURCE_URL |
Where the "Powered by Maildun" notice links | Leave the default on an unmodified copy; if you run a modified version for users over a network, point it at your own complete corresponding source (see License obligations) |
PASSKEYS_USER_HANDLE_SECRET |
Derives the WebAuthn user handle | Set it once to its own random, stable value; when empty or absent it falls back to APP_KEY |
REGISTRATION_ENABLED |
Allows public account creation | Prefer false unless open registration is intentional |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Enables "Continue with Google" | Leave empty to hide the button; set both from a Google Cloud OAuth client |
GOOGLE_REDIRECT_URI |
Callback URL sent to Google | Leave unset; it defaults to APP_URL + /auth/google/callback |
TRUSTED_PROXIES |
Trusts proxy-provided client addresses | Use explicit proxy addresses when possible; never set * on a directly exposed server |
Invited teammates can finish registration even when public registration is disabled.
"Continue with Google" appears on the login, registration, and password confirmation
screens once both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set; with either one
empty the button is hidden and every Google route responds 404. In Google Cloud Console,
create a Web application OAuth client and add {APP_URL}/auth/google/callback as an
authorized redirect URI. Google sign-in follows the same rules as the register form:
new accounts need REGISTRATION_ENABLED=true or a pending workspace invitation for that
address, none can be created before installation finishes, and each starts with its
own personal workspace. Only addresses Google
reports as verified are accepted. An existing account is linked by email only if its own
address is verified, and after that it is matched by its Google account ID. Two-factor
authentication still applies, and people who signed up with Google can set a password
later from Security settings.
APP_URL carries more weight than its name suggests. Queue workers serve no request and
cannot infer the host, so open pixels, click redirects, and unsubscribe links are signed
against this value; changing it invalidates the links in every message already delivered.
Its host is also the passkey relying party ID, so changing the host invalidates every
registered passkey, whatever PASSKEYS_USER_HANDLE_SECRET is set to.
PASSKEYS_USER_HANDLE_SECRET keys the HMAC that derives the WebAuthn user handle given to
an authenticator when a passkey is registered. Left empty, as the
PASSKEYS_USER_HANDLE_SECRET= line in .env.example ships, or removed, it falls back to
APP_KEY. Set it once to its own random, stable value, for example from
openssl rand -base64 32, so the handle stays the same if APP_KEY is ever rotated.
The secret is used only at registration. Sign-in checks the handle stored with each
passkey, so changing the secret, or rotating APP_KEY while it is unset, leaves existing
passkeys working; only passkeys registered afterwards get a handle derived from the new
value. Passkeys registered by earlier releases, which used an empty secret when the line
was left blank, also keep working.
SQLite is convenient locally and is the default. PostgreSQL and MySQL/MariaDB are also supported — set DB_CONNECTION and uncomment the connection details in .env.example. Use a managed or operationally backed-up database for production workloads.
Maildun defaults to Redis for cache and queues. Horizon consumes four queue groups:
defaultfor general application work;trackingfor engagement processing;transactionalfor transactional messages; andcampaignsfor bulk sends.
The MAIL_*_QUEUE variables can point all work to default for a small installation. Horizon process caps and wait thresholds are controlled by the HORIZON_* variables in .env.example.
Each workspace configures one provider under Settings → Email delivery. Those credentials are encrypted in the database with APP_KEY. Delivery is tested independently with explicit From and To addresses; workspace sender addresses are verified separately against the current connection version.
The application-wide MAIL_* values are used for system notifications, never as a fallback for workspace campaigns or transactional email. MAIL_SES_* belongs to the optional platform-wide SES mailer. It is deliberately separate from the AWS_* and R2_* object-storage settings.
See Amazon SES and SMTP.
Set MAIL_RATE_LIMIT_PER_SECOND to a value within the provider's accepted send rate. The limit is shared by campaign, transactional and automation email. A value of 0 disables application-side limiting. MAIL_RATE_LIMIT_RELEASE_AFTER controls how many seconds a send job waits before asking for a slot again.
FILESYSTEM_DISK=local stores uploads on the server. Run php artisan storage:link and include uploaded files in backups.
For cloud storage, set FILESYSTEM_DISK=s3, choose OBJECT_STORAGE_PROVIDER=aws or OBJECT_STORAGE_PROVIDER=r2, and configure private and public buckets separately.
AWS S3 uses:
AWS_BUCKETstores attachments and other private files;AWS_PUBLIC_BUCKETstores public media, logos, and avatars;AWS_PUBLIC_URLoptionally points to a public CDN or custom origin; andAWS_ENDPOINTremains empty for AWS S3.
Cloudflare R2 uses the equivalent dedicated R2_BUCKET, R2_PUBLIC_BUCKET, and R2_PUBLIC_URL values. Set R2_ENDPOINT to the account's S3 API endpoint. Access keys are likewise separate as AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY and R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY.
For another S3-compatible provider, select the custom provider in php artisan storage:configure; it uses the AWS_* section with AWS_ENDPOINT and AWS_PUBLIC_URL.
Use php artisan storage:configure to validate a backend and php artisan storage:sync --dry-run before migrating files.
Maildun stores first-party open and click events, including technical request metadata. MAIL_TRACKING_EVENT_RETENTION_DAYS defaults to 90 days; aggregated campaign insights remain after detailed events are pruned. Set it to the shortest period your use case requires, or 0 only when indefinite detailed retention is intentional.
Geolocation uses local DB-IP Lite .mmdb files configured with MAIL_TRACKING_CITY_DATABASE and MAIL_TRACKING_ASN_DATABASE. Leave them empty to use storage/app/private/geoip/dbip-city-lite.mmdb and dbip-asn-lite.mmdb. Relative paths are resolved from the project root. These databases are not included in the repository; Set MAIL_TRACKING_GEOLOCATION_AUTO_UPDATE_ENABLED=true to let php artisan emails:update-geolocation-databases (scheduled daily) download the current DB-IP Lite City and ASN files into those paths. It is off by default because it contacts download.db-ip.com and replaces any database already at those paths.
Outside local development, /horizon is limited to the comma-separated accounts in HORIZON_ALLOWED_EMAILS. Leave no production dashboard accidentally public.
Set HORIZON_NOTIFICATION_EMAIL to receive long-wait alerts and keep the scheduled horizon:snapshot command running for metrics.
Maildun provides a local MCP server through .mcp.json and a remote OAuth-protected endpoint at /mcp/maildun. The remote endpoint requires the mcp:use scope and is restricted to the authenticated user's workspace memberships. Keep APP_URL accurate, allow only trusted client callback origins in MCP_REDIRECT_DOMAINS, and never use a wildcard for a network-accessible deployment.
The default callback origins are https://chatgpt.com and https://grok.com. Native callback schemes are configured separately in config/mcp.php; only cursor is enabled by default. Access tokens expire after MCP_ACCESS_TOKEN_EXPIRATION_MINUTES (default 60), and refresh tokens after MCP_REFRESH_TOKEN_EXPIRATION_DAYS (default 30).
Remote OAuth needs a persistent Passport key pair; app:install does not generate it. See Using MCP for key setup, local and remote connection instructions, supported tools, and troubleshooting.
The design assistant is off until at least one of OPENAI_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY is set. Each key adds that provider's models to the picker; override the model IDs with ASSISTANT_OPENAI_MODEL, ASSISTANT_ANTHROPIC_MODEL, ASSISTANT_ANTHROPIC_FAST_MODEL, and ASSISTANT_GEMINI_MODEL. It appears only in the Email Builder editor for campaigns, transactional emails, and templates, and only to users who can manage the item. The same models write preheaders from the preheader field's generate button.
Each workspace may make ASSISTANT_DAILY_LIMIT requests per rolling 24 hours (default 100; 0 means unlimited). Requests are also throttled to 20 per minute. The assistant needs Node.js and the installed JavaScript packages on the server, like the MCP email builder tools (see Production), and it sends prompts and email design content to the selected third-party AI provider. Run php artisan assistant:check to confirm that a model is configured, the builder runtime works, and a model call succeeds.
Never commit .env, provider credentials, API keys, database dumps, OAuth keys, .mmdb databases, or generated storage content. Prefer the hosting platform's secret manager and restrict access to backups and logs.