# Configuration reference

> Every setting InfraInbox reads, its environment variable, its default and what it does.

Web page: https://infrainbox.app/docs/self-hosting/configuration/

InfraInbox reads one typed configuration, built up in this order — each later source overrides the same setting from an earlier one:

1. built-in defaults;
2. a YAML file (`--config <file>`, or the file named by `INFRAINBOX_CONFIG`);
3. environment variables — a few plain aliases (`PUBLIC_URL`, `DATABASE_URL`) first, then every `INFRAINBOX_*` variable, then the `_FILE` variant of any secret setting;
4. command-line flags, such as `serve --mode`.

Every setting has an `INFRAINBOX_` environment variable: its YAML path in upper case with dots turned into underscores, so `database.url` is `INFRAINBOX_DATABASE_URL`. A handful use a shorter alias instead (noted below). Secret settings (marked **secret** in the tables) also accept a `_FILE` variant that names a file to read the value from — prefer it over the plain variable for anything sensitive, since a plain environment variable is readable from `docker inspect`, a process listing, and the shell history of whoever set it.

To see the configuration InfraInbox actually loaded — every value, its source, and secrets redacted — run:

```bash
docker compose exec infrainbox /infrainbox config check
```

A YAML file mirrors the same nesting, for example:

```yaml
auth:
  password: enabled
  require_mfa: owners
retention:
  events: 30d
```

Durations are Go-style (`90s`, `15m`, `24h`) or a whole number of days (`30d`); `0` means "keep forever" where that's meaningful.

## Core and networking

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_MODE` | `self-hosted` | `self-hosted` or `cloud`. There is no reason to set this yourself — it's what makes the same binary power InfraInbox Cloud, and a few settings below (`outbound.allow_cgnat`, `insecure_http`, the master key) are refused or required differently under `cloud`. |
| `INFRAINBOX_PUBLIC_URL` (alias `PUBLIC_URL`) **required** | — | The address browsers and phones reach this server at, scheme and host (and port, unless 443). Sign-in, the pairing QR and every generated ingest URL are built from it. |
| `INFRAINBOX_INGEST_URL` | same as `public_url` | Base URL that generated source configurations (webhook targets, curl snippets) send events to. Only worth setting separately behind some unusual split-DNS setups. |
| `INFRAINBOX_DATA_DIR` | `/var/lib/infrainbox` | Where the master key and any generated TLS certificate live. Back it up with the database. |
| `INFRAINBOX_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error`. Event and notification bodies are logged only at `debug`. |
| `INFRAINBOX_SERVE_MODE` | `all` | Which components this process runs: `all` (everything, the only sensible choice for a single self-hosted instance), or `api`/`worker` to split them across processes. Also settable as `serve --mode`. |
| `INFRAINBOX_SERVE_STOP_GRACE` | `30s` | How long shutdown has to finish in-flight work before the process exits. Match your container platform's stop grace period (Compose's `stop_grace_period: 30s` already does). |
| `INFRAINBOX_MIGRATIONS_AUTO` | `true` | Apply pending database migrations automatically when the server starts. Turn this off only if you'd rather run `infrainbox migrate up` yourself as a separate step — with it off, `serve` refuses to start while migrations are pending. |

## Database

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_DATABASE_URL` (alias `DATABASE_URL`) **required, secret** | — | The PostgreSQL connection URL, e.g. `postgres://infrainbox:pw@postgres:5432/infrainbox`. Prefer `INFRAINBOX_DATABASE_URL_FILE`. |
| `INFRAINBOX_DATABASE_APP_MAX_CONNS` | `20` | Maximum connections used by the API/ingest side. |
| `INFRAINBOX_DATABASE_WORKER_MAX_CONNS` | `10` | Maximum connections used by delivery workers and the scheduler. |

## The master key

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_MASTER_KEY` (prefer `_FILE`) **secret** | generated | 32 bytes, base64 (`openssl rand -base64 32`). Encrypts every stored secret (destination credentials, push keys, and so on). Left unset, one is generated into `<data_dir>/master.key` on the first start of a fresh database — back it up, apart from the database dump. |
| `INFRAINBOX_MASTER_KEY_PREVIOUS` (prefer `_FILE`) **secret** | — | Set during a key rotation: the key you're rotating away from, while `master_key` holds the new one. See [Backups and upgrades](https://infrainbox.app/docs/self-hosting/backup-and-upgrade.md#rotating-the-master-key). |

## HTTP and TLS

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_HTTP_LISTEN` | `:8443` (`:8080` if `tls` is `off`) | Address of the main listener. |
| `INFRAINBOX_TLS` | `on` | `on`: HTTPS, with `cert_file`/`key_file` or a generated self-signed certificate. `off`: plain HTTP behind a TLS-terminating proxy. |
| `INFRAINBOX_TLS_CERT_FILE` | — | PEM certificate chain for the main listener. |
| `INFRAINBOX_TLS_KEY_FILE` | — | PEM private key matching `cert_file`. Set both or neither. |
| `INFRAINBOX_TLS_CA_FILE` | — | PEM CA certificate of a private CA; its pin is included in the pairing QR code. |
| `INFRAINBOX_INSECURE_HTTP` | `off` | `private`: also serve plain HTTP on the HTTPS port, to callers on private-IP ranges only (for LAN phones that can't validate a private CA). Needs `tls: on`. |
| `INFRAINBOX_HTTP_TRUSTED_PROXIES` | none (`tls: on`) / RFC 1918 + loopback (`tls: off`) | IP addresses or CIDR ranges whose `X-Forwarded-*` headers are trusted for the client's real IP and scheme. Set this to your reverse proxy's own address — never a whole subnet — or every rate-limit bucket collapses into one. See [TLS and reverse proxies](https://infrainbox.app/docs/self-hosting/reverse-proxy.md). |
| `INFRAINBOX_HTTP_TRUSTED_HOSTS` (alias `TRUSTED_HOSTS`) | none | Extra hostnames (comma-separated, no scheme/port/wildcard) the dashboard and API answer to besides `public_url`'s. Anything else gets a 421. |
| `INFRAINBOX_HTTP_READ_HEADER_TIMEOUT` | `10s` | Longest wait for a request's headers. |
| `INFRAINBOX_HTTP_READ_TIMEOUT` | `1m` | Longest time to read a whole request. |
| `INFRAINBOX_HTTP_WRITE_TIMEOUT` | `1m` | Longest time to write a response (the live event stream is exempt). |
| `INFRAINBOX_HTTP_IDLE_TIMEOUT` | `2m` | How long an idle keep-alive connection stays open. |
| `INFRAINBOX_HTTP_MAX_HEADER_BYTES` | `65536` | Largest request header block, in bytes. |

## Metrics

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_METRICS_LISTEN` | `127.0.0.1:9464` | Address of the internal `/metrics` listener (Prometheus format). Not published by the compose file by default. |
| `INFRAINBOX_METRICS_TOKEN` **secret** | — | Set this to also serve `/metrics` on the main, public listener, guarded by this bearer token, instead of only the internal one. |

## Observability (OpenTelemetry)

Off by default self-hosted. Set `otel.endpoint` to export traces to a collector you run yourself (Jaeger, Grafana Tempo, and so on) — see [Security and privacy](https://infrainbox.app/docs/self-hosting/security.md#what-leaves-the-server) for what this does and doesn't send anywhere.

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_OTEL_TRACES` | `false` (`true` if `otel.endpoint` is set) | Export traces over OTLP. |
| `INFRAINBOX_OTEL_ENDPOINT` (alias `OTEL_EXPORTER_OTLP_ENDPOINT`) | — | Your OTLP collector, e.g. `https://otel.example.com:4318`. |
| `INFRAINBOX_OTEL_HEADERS` (alias `OTEL_EXPORTER_OTLP_HEADERS`) **secret** | — | Headers sent with every OTLP export, `key=value` pairs separated by commas — often an API key your collector wants. |

## Retention

How long data is kept before it's purged. `0` keeps it forever; open incidents are never purged regardless of `retention.incidents`.

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_RETENTION_EVENTS` | `30d` | Raw events (the first and last event of a retained incident are always kept). |
| `INFRAINBOX_RETENTION_INCIDENTS` | `90d` | Resolved incidents, counted from resolution. |
| `INFRAINBOX_RETENTION_DELIVERIES` | `30d` | Delivery attempt records. |
| `INFRAINBOX_RETENTION_RAW_PAYLOADS` | `7d` | Raw inbound payloads kept for debugging. |
| `INFRAINBOX_RETENTION_RAW_PAYLOADS_PER_SOURCE` | `20` | Cap on raw payloads kept per source, on top of the age limit above. |
| `INFRAINBOX_RETENTION_HEARTBEAT_PINGS_PER_MONITOR` | `100` | Individual heartbeat pings kept per monitor. |
| `INFRAINBOX_RETENTION_HEARTBEAT_AGGREGATES` | `90d` | Daily heartbeat summaries. |
| `INFRAINBOX_RETENTION_IDEMPOTENCY_KEYS` | `1d` | How long a duplicate event with the same idempotency key is still recognized (minimum `24h`). |
| `INFRAINBOX_RETENTION_AUDIT` | `365d` | Audit log entries. |
| `INFRAINBOX_RETENTION_EXPIRED_SESSIONS` | `30d` | Expired sessions and pairing tokens, counted from expiry. |

## Delivery concurrency

How many in-flight sends each worker allows per transport, so one slow provider doesn't starve the others. (A few more of these keys exist in the configuration loader than are listed here — they belong to destinations this build doesn't ship yet, so there's nothing to point them at.)

| Setting | Default |
|---|---|
| `INFRAINBOX_DELIVERY_CONCURRENCY_TELEGRAM` | `8` |
| `INFRAINBOX_DELIVERY_CONCURRENCY_SLACK` | `8` |
| `INFRAINBOX_DELIVERY_CONCURRENCY_DISCORD` | `8` |
| `INFRAINBOX_DELIVERY_CONCURRENCY_WEBHOOK` | `16` |
| `INFRAINBOX_DELIVERY_CONCURRENCY_PUSH` | `32` |

## Push notifications

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_PUSH_WEBPUSH_VAPID_KEY` (prefer `_FILE`) **secret** | `auto` | The server's Web Push signing key. `auto` generates one on first start and stores it encrypted in the database. |
| `INFRAINBOX_PUSH_WEBPUSH_SUBJECT` | — | A contact for push services about this server's traffic: `mailto:you@example.com` or an `https://` URL. |
| `INFRAINBOX_PUSH_RELAY_ENABLED` | `true` | Advertise InfraInbox's push relay to pairing devices (see [Security and privacy](https://infrainbox.app/docs/self-hosting/security.md#push-notifications)). |
| `INFRAINBOX_PUSH_RELAY_URL` | `https://push.infrainbox.app` | The relay's base URL. |
| `INFRAINBOX_PUSH_APNS_ENABLED` | `false` | Send to Apple Push Notification service directly, with your own key, instead of through the relay — for a self-built iOS client. Needs the three settings below. |
| `INFRAINBOX_PUSH_APNS_KEY` (prefer `_FILE`) **secret** | — | The APNs authentication key (`.p8`, PEM). |
| `INFRAINBOX_PUSH_APNS_KEY_ID` | — | The key's 10-character key ID. |
| `INFRAINBOX_PUSH_APNS_TEAM_ID` | — | Your Apple developer team ID. |

## Email

InfraInbox mails account links — password reset, the confirmation of a changed address — through an SMTP relay you name: a mail provider's SMTP endpoint, or a local MTA. Without `smtp.host` nothing is mailed: **Forgot password?** doesn't appear, an email change is refused, and `infrainbox admin reset-password` is how a password gets reset. Mail goes out after the page has answered, so a slow relay never slows sign-in, and the server logs only which kind of mail went out, never the address or the link.

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_SMTP_HOST` | — | The relay's host name. Setting it turns account mail on. |
| `INFRAINBOX_SMTP_PORT` | by `tls` | The relay's port; unset picks 587 for `starttls`, 465 for `tls`, 25 for `none`. |
| `INFRAINBOX_SMTP_USERNAME` | — | The relay's user name; empty sends without logging in. |
| `INFRAINBOX_SMTP_PASSWORD` (prefer `_FILE`) **secret** | — | The relay's password. |
| `INFRAINBOX_SMTP_TLS` | `starttls` | `starttls` (required, not optional), `tls` (TLS from the first byte) or `none` (plain text, for a relay on the same host or network only). |
| `INFRAINBOX_SMTP_FROM` | — | The sender, e.g. `InfraInbox <no-reply@example.com>`. Required with a host. |
| `INFRAINBOX_SMTP_ALERTS_HOST`, `_PORT`, `_USERNAME`, `_PASSWORD`, `_TLS`, `_FROM` | — | A separate relay and sender for alert mail, so alert volume can never hold up a password reset. Unset, alert mail uses the relay above. |

To check the relay, run `docker compose exec infrainbox /infrainbox config check`, then use **Forgot password?** on the sign-in page with your own address.

## Outbound connections

Destinations, webhooks and test sends may always reach public addresses and, self-hosted, private (RFC 1918) ones. Loopback, link-local, multicast and cloud-metadata addresses are never allowed — see [Security and privacy](https://infrainbox.app/docs/self-hosting/security.md#outgoing-connections-and-ssrf-protection).

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_OUTBOUND_ALLOW_CGNAT` | `false` | Also allow carrier-grade NAT addresses (`100.64.0.0/10`) — needed if a destination lives on a Tailscale tailnet reached that way. |
| `INFRAINBOX_OUTBOUND_ALLOW_ULA` | `false` | Also allow IPv6 unique local addresses (`fc00::/7`). |

## Authentication

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_AUTH_PASSWORD` | `enabled` | Who may sign in with a password: `enabled` (everyone), `admins_only`, or `disabled`. `infrainbox admin reset-password` always works regardless, so a broken identity provider never locks you out. |
| `INFRAINBOX_AUTH_REQUIRE_MFA` | `none` | Who must set up a second factor: `none`, `owners` (workspace owners and instance admins), or `all`. |
| `INFRAINBOX_AUTH_PASSWORD_HASH_PROFILE` | `default` | Argon2id cost: `default` (fine down to a Raspberry Pi / 1 GB host) or `large` (bigger hosts). |
| `INFRAINBOX_AUTH_PASSWORD_HASH_MEMORY_BUDGET_MIB` | `128` | Total memory concurrent password hashing may use; sign-ins beyond it wait briefly, then get rate-limited. |
| `INFRAINBOX_AUTH_ADMIN_EMAIL` | — | Seeds the first instance administrator's email at start, for an unattended install. Only applies while the instance has no user yet. |
| `INFRAINBOX_AUTH_ADMIN_PASSWORD_FILE` **secret, file only** | — | The seeded administrator's password. There's deliberately no plain-variable form for this one. |

### Sign in with an identity provider

Each entry in `auth.providers` is an OpenID Connect provider (Authentik, Keycloak, Authelia, Pocket ID and the like) or GitHub that people can sign in through. It's a list, so it lives in the config file:

```yaml
auth:
  providers:
    - id: authentik               # part of the callback URL; keep it stable
      type: oidc                  # oidc or github
      name: Authentik             # the button's label (default: the id; "GitHub" for github)
      issuer: https://auth.example.com/application/o/infrainbox/
      client_id: infrainbox
      client_secret_file: /run/secrets/oidc   # optional for oidc (a public client uses PKCE); required for github
      auto_create: allowed_domains            # off (default), invite, allowed_domains or on
      allowed_email_domains: [example.com]
      scopes: [groups]            # extra scopes; openid, email and profile are always asked for
```

- **Callback URL.** Register `https://infrainbox.example.com/v1/auth/oidc/<id>/callback` at the provider, built from `INFRAINBOX_PUBLIC_URL`. For GitHub, that's the callback of a GitHub OAuth App.
- **Who gets an account.** A first sign-in creates an account only if `auto_create` allows it: `off` never (an administrator creates or invites the person first), `invite` for an address with a pending invite, `allowed_domains` for a verified address in `allowed_email_domains`, `on` always. The first administrator is always created with the setup link: until setup is done, no provider creates an account.
- **Matching.** An account is found by the provider's own subject, never by email, so an address changed at the provider can't take over someone else's account.
- **Keep ids stable.** Changing a provider's `issuer` or `type` under an id that already has accounts stops the server from starting. Add the new provider under a new id instead.
- **Plain HTTP.** An `http://` issuer passes the server's checks but logs a warning on every start: the provider's keys and tokens then travel unencrypted. The dashboard's sign-in page only sends the browser to an `https://` authorization address, so the provider's own pages must be served over HTTPS.
- **GitHub and sensitive actions.** GitHub can't re-confirm a sensitive action; someone who signs in only with GitHub is asked to add a passkey or TOTP for that.
- **Check it.** `infrainbox admin idp test <id>` fetches the provider's discovery document and signing keys and says what's wrong.

- **On the sign-in page.** Each provider gets a "Continue with <name>" button under the password form; with `auth.password: disabled` the buttons are all there is. An account with TOTP is asked for its code after the provider, as after a password. A refused sign-in comes back with one plain reason — no account and `auto_create` doesn't allow one, the address belongs to an account the identity isn't linked to, the provider couldn't be reached — and the details are in the audit log only.
- **On the setup page.** The first administrator can be your identity at a provider instead of a password: open the setup link, fill in the workspace, and choose "Continue with <name>". The setup token is spent when the provider sends you back.

One OpenID Connect provider can also come from the environment, for an install without a config file. Its id is `oidc`, and it's on when the issuer is set:

| Setting | Default | Meaning |
|---|---|---|
| `INFRAINBOX_OIDC_ISSUER` | — | The issuer URL, exactly as the provider's discovery document states it. |
| `INFRAINBOX_OIDC_CLIENT_ID` | — | The client ID registered at the provider. |
| `INFRAINBOX_OIDC_CLIENT_SECRET_FILE` **secret, file only** | — | The client secret. Leave it unset for a public client. |
| `INFRAINBOX_OIDC_NAME` | `SSO` | The button's label. |
| `INFRAINBOX_OIDC_AUTO_CREATE` | `off` | As `auto_create` above. |
| `INFRAINBOX_OIDC_ALLOWED_EMAIL_DOMAINS` | — | Comma-separated, as `allowed_email_domains` above. |

## Workspace limits

Per-workspace quotas. `0` means no limit, which is the self-hosted default for all of them.

| Variable | Default | Meaning |
|---|---|---|
| `INFRAINBOX_QUOTAS_EVENTS_PER_DAY` | `0` | Events a workspace may ingest per UTC day. Past it, ingest answers HTTP 429 with `Retry-After` until midnight UTC. |
| `INFRAINBOX_QUOTAS_SOURCES` | `0` | Sources a workspace may have. |
| `INFRAINBOX_QUOTAS_MONITORS` | `0` | Heartbeat monitors a workspace may have. |
| `INFRAINBOX_QUOTAS_DESTINATIONS` | `0` | Destinations, not counting the built-in push one. |
| `INFRAINBOX_QUOTAS_MEMBERS` | `0` | Members of a workspace. |
| `INFRAINBOX_QUOTAS_DELIVERIES_PER_DAY` | `0` | Notifications a workspace may have sent per UTC day, across every destination. Re-notify and fallback sends count. Past it, a notice is recorded `skipped` with reason `quota` rather than sent. |
| `INFRAINBOX_QUOTAS_DELIVERIES_PER_DESTINATION_PER_DAY` | `0` | The same, for any one destination on its own. |
| `INFRAINBOX_QUOTAS_NEW_WORKSPACE_DELIVERIES_PER_DAY` | `0` | A lower daily notification total for a workspace's first seven days. A per-workspace override wins over it. |
| `INFRAINBOX_QUOTAS_WORKSPACES` | `0` | Workspaces one account may own. It does not limit the workspaces somebody is invited into. |

Creating one more of something past its limit answers HTTP 409 `quota_exceeded`. `infrainbox admin workspaces limits <workspace>` shows one workspace's limits, and its flags (`--events-per-day`, `--deliveries-per-day`, `--sources`, … and `--clear`) override the defaults for that workspace alone. Every workspace's counts against its limits are on its [Usage and limits](https://infrainbox.app/docs/reference/usage-and-limits.md) panel.

## Rate limits

Per-instance token buckets; a request beyond the limit gets HTTP 429 with `Retry-After`. Each row is `INFRAINBOX_RATELIMIT_<NAME>_PER_MINUTE` and `..._BURST`.

| Bucket | Default (per minute / burst) | Applies to |
|---|---|---|
| `INGEST_KEY` | 600 / 300 | One source's ingest key. |
| `INGEST_WORKSPACE` | 3000 / 1000 | A whole workspace's ingest traffic. |
| `AUTH_FAILURES` | 30 / 20 | Failed credentials, per client IP. |
| `MANAGEMENT` | 1200 / 200 | The management API, per signed-in user. |
| `PUBLIC` | 120 / 60 | Unauthenticated routes (`/v1/info`, sign-in pages), per client IP. |
| `DESTINATION_TEST` / `DESTINATION_TEST_WORKSPACE` | 6 / 3 and 30 / 10 | "Send test" on a destination. |
| `TELEGRAM_DISCOVER` | 30 / 10 | The Telegram chat-discovery poll while setting up that destination. |
| `SOURCE_TEST` | 6 / 3 | "Test" on a source. |
| `HEARTBEAT_MONITOR` | 60 / 10 | Pings to one heartbeat monitor. |
| `AUTH_LOGIN` | 30 / 15 | `POST /v1/auth/login`, per client IP. |
| `AUTH_TOTP` | 30 / 15 | TOTP code verification. |
| `AUTH_REAUTH` | 30 / 15 | Step-up re-authentication. |
| `AUTH_SETUP` | 20 / 10 | First-run setup. |
| `AUTH_SIGNUP` | 10 / 10 | Cloud sign-up, and opening a link that confirms an address, per client IP. |
| `AUTH_RESET` | 10 / 10 | Asking for a password reset link, and using one, per client IP. |
| `AUTH_MAIL` | 1 / 3 | Account mail to one address or account — reset links, re-sent confirmations — whoever asks. |

## Development-only settings

`dev.telegram_api_url`, `dev.apns_url`, `dev.fcm_url` and `dev.relay_url` point delivery transports at a local test double, and `dev.github_url` does the same for GitHub sign-in, instead of the real provider. They exist for InfraInbox's own CI and are refused outright if `mode` is `cloud`. **Don't set these on a real instance** — doing so sends provider traffic (including credentials) to whatever URL you named, and the server logs a warning every time it starts with one set.
