Environment Variables
Complete reference for .env.production. Copy .env.production.example and fill in every value. Never commit .env.production to git.
:::info One deployment, all tenants These variables configure the platform (one shared deployment), not a single customer. Per-customer settings — mailboxes, calendar, roles — are stored per tenant in the database and configured in-app, not here. :::
Database
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string for the shared database that holds all tenants, e.g. postgresql://user:pass@host:5432/db?sslmode=require |
Microsoft Entra ID (platform SSO)
From the platform Azure App Registration:
| Variable | Description |
|---|---|
ENTRA_TENANT_ID | Directory (tenant) ID of the platform SSO app |
ENTRA_CLIENT_ID | Application (client) ID |
ENTRA_CLIENT_SECRET | Client secret value |
:::note Email-to-ticket uses this same registration
Earlier versions let each tenant bring its own Azure app registration for mail. That was removed — email now authenticates with this app against each tenant's registered Entra directory. The app-only connection mode additionally requires Mail.ReadWrite and Mail.Send application permissions on it.
:::
Azure Blob Storage (attachments)
| Variable | Description |
|---|---|
AZURE_STORAGE_ACCOUNT_NAME | Storage account name |
AZURE_STORAGE_ACCOUNT_KEY | Storage account access key |
AZURE_STORAGE_CONTAINER_NAME | Blob container for attachments (e.g. attachments) |
Application
| Variable | Description |
|---|---|
APP_URL | Public HTTPS URL of the instance — must be reachable from the internet (Graph webhooks call it) |
TEAMS_APP_ID | Teams app ID from the manifest |
NEXT_PUBLIC_TEAMS_APP_ID | Same ID, exposed to the client bundle |
Authentication
| Variable | Description |
|---|---|
BETTER_AUTH_SECRET | Session signing secret — generate with openssl rand -hex 32 |
BETTER_AUTH_URL | Public base URL Better Auth issues callbacks against — normally the same as APP_URL |
ENTRA_SSO_AUTHORITY | Azure authority for web SSO. Defaults to common (multi-tenant). Set to a specific directory id to lock sign-in to one directory |
ENTRA_TID_ENFORCE | true makes an Azure directory mismatch block the sign-in. Default (anything else) logs it and allows it — see Directory verification |
:::caution Turn on ENTRA_TID_ENFORCE deliberately
With it off, a Microsoft account from the wrong Azure directory can still sign in — the mismatch is only logged. Register each tenant's entraDirectoryId first, watch the logs for mismatches, then enable it.
:::
Outbound Email (SMTP)
Used for acknowledgements and notifications to custom-login users:
| Variable | Description |
|---|---|
SMTP_HOST / SMTP_PORT / SMTP_SECURE | SMTP server (e.g. smtp.office365.com, 587, false) |
SMTP_USER / SMTP_PASS | SMTP credentials |
SMTP_FROM | From address on outbound mail |
:::note INTERNAL_DOMAINS is derived, not set
The platform's internal domain is derived automatically from the domain of SMTP_USER (e.g. admin@example.com → example.com) — there is no separate variable. Platform-admin eligibility is likewise restricted to the operator's own email domain (a build-time constant), so choose the SMTP sender accordingly.
:::
Email-to-Ticket
| Variable | Description |
|---|---|
GRAPH_WEBHOOK_SECRET | Random string verifying Graph change notifications — generate with openssl rand -hex 16 |
CRON_SECRET | Bearer token the worker uses to call /api/cron/email. Falls back to GRAPH_WEBHOOK_SECRET when unset, so existing deployments keep working |
:::danger Without one of these, mail does not flow
The email cycle refuses unauthenticated triggers. If neither CRON_SECRET nor GRAPH_WEBHOOK_SECRET is set, the worker skips the job entirely and logs a warning — Graph subscriptions are never renewed and polled inboxes report "Not yet polled" forever.
A 401 in the worker's [email-cycle] logs means the worker's secret disagrees with the web app's.
:::
Credential Encryption
| Variable | Description |
|---|---|
EMAIL_ENCRYPTION_KEY | AES-256-GCM key, exactly 64 hex chars — openssl rand -hex 32. Encrypts stored mailbox credentials: delegated OAuth refresh tokens and IMAP/SMTP passwords |
Changing EMAIL_ENCRYPTION_KEY after go-live invalidates every tenant's stored mailbox credentials — every delegated connection and IMAP password must be re-entered. Store it in a secrets manager and never rotate it casually.
:::note Also required by the web app
The Next.js app additionally reads BETTER_AUTH_SECRET (session signing), the AZURE_STORAGE_* variables (attachments), and APP_URL / TEAMS_APP_ID. Set all of them for a working deployment.
:::