Configuration
All services read one shared .env at the repository root. Config classes read it through getenv and fail fast on a missing key, so a misconfigured service refuses to boot rather than misbehaving later.
Start from the template:
cp .env.example .envCore
| Variable | Notes |
|---|---|
DOMAIN | Base domain for every subdomain router. localhost in development. |
APP_ENV | development or production |
APP_LOG_LEVEL | Monolog level, e.g. debug, error |
APP_MODE | selfhosted or hosted |
ACCESS_CONTROL_ALLOW_ORIGIN | CORS origin. In production set this to the admin origin, not *. |
ROOT_ADMIN_EMAILS | Comma-separated bootstrap site admins. Promoted to a persisted flag on login. |
REGISTRATION_MODE | Who may create an account: open, invite or closed. See below. |
Registration
REGISTRATION_MODE decides who may create an account on the instance. It gates account creation only — signing in, accepting an organization invitation and linking an SSO identity to an account that already exists are unaffected.
| Value | Who gets in |
|---|---|
open (default) | Anyone. The signup form is on the sign-in screen. |
invite | Only an address a site admin has invited from the admin panel. The invite is consumed on use, so it works exactly once. |
closed | Nobody, outstanding invites included. Issuing new invites is refused too, since they could not be redeemed. |
The switch applies to every way an account is created — POST /auth/register and GitHub SSO alike. An SSO sign-in for an unknown GitHub account is refused before the "create an account" step is offered, and in invite mode the invite is matched against the provider's verified email, since OAuth returns an address rather than an invite token.
The dashboard reads GET /auth/registration and hides what it cannot offer, so a closed instance shows a sign-in form with no way to register. An unrecognised value falls back to open rather than locking an instance out of its own signup.
Secrets
Generate each of these; never ship the defaults.
| Variable | Generate with |
|---|---|
APP_ENCRYPTION_KEY | openssl rand -hex 32 |
JWT_SECRET | openssl rand -hex 32 |
INTERNAL_API_SECRET | openssl rand -hex 32 |
APP_ENCRYPTION_KEY is not rotatable in place
It encrypts stored channel credentials and is the root of the unsubscribe link signing key. Changing it invalidates every opt-out link already sitting in delivered mail, and makes stored channel credentials unreadable.
Datastores
| Variable | Notes |
|---|---|
DB_NAME, DB_USER, DB_PASSWORD | PostgreSQL |
RABBITMQ_USER, RABBITMQ_PASSWORD | Broker |
INTERNAL_API_URL | Public API's address on the internal network |
Mail
SMTP_* configures the outbound relay. In development it points at MailPit, which captures everything at mailpit.<DOMAIN> and delivers nothing.
Optional
| Variable | Notes |
|---|---|
GITHUB_OAUTH_CLIENT_ID / _SECRET | Enables "Continue with GitHub". Leave empty to hide the buttons. |
KEY_EXPIRY_WARN_DAYS | How early to warn about expiring API keys |
MESSAGE_RETENTION_DAYS | Message retention window in selfhosted mode |