# Configuration

The operator reads its configuration from a TOML file, environment variables, and CLI flags. This page documents every configuration field, its type, default value, and behavior.

## Loading order

Configuration is merged using [Figment](https://docs.rs/figment/) with the following precedence (highest wins):

1. **CLI flags** -- `--bind`, `--wg-port`, etc.
2. **Environment variables** -- prefixed with `AIRDRESS_`
3. **Config file** -- TOML at the path given by `--config` or `AIRDRESS_CONFIG`
4. **Compiled defaults** -- built into the binary

The default config file path is `/etc/airdress/operator.toml`. Override it with:

```sh
airdress-operator -c ./my-config.toml serve
```

Validate the resolved configuration at any time:

```sh
airdress-operator config        # print merged result
airdress-operator config --check # validate without printing (exit 0 = OK)
```

## Full annotated example

This example shows every section with representative values. Fields set to their defaults are commented.

```toml
# Top-level settings
# bind = "127.0.0.1:8080"   # HTTP listener address
# wg_port = 51820            # WireGuard UDP port
# name = "airdress-operator" # Instance name (appears in logs)

[dns]
enabled = true
# bind_udp = "0.0.0.0:53"
# bind_tcp = "0.0.0.0:53"
zone = "humans.airdress.co"

[dns.soa]
mname = "ns1.airdress.co"
rname = "noc.airdress.co"

[dns.cache]
# max_entries = 10000
# default_ttl = 60

[dns.ratelimit]
# queries_per_second = 1000
# burst = 200

[[dns.records]]
name = "myapp.humans.airdress.co"
type = "A"
rdata = "192.168.1.10"
ttl = 300

[[dns.records]]
name = "api.humans.airdress.co"
type = "A"
rdata = "192.168.1.11"
# ttl = 60

[auth]
issuer = "https://auth.airdress.co"
jwks_uri = "https://auth.airdress.co/.well-known/jwks.json"
audience = "airdress-operator"

[database]
# url = "postgresql://user:pass@localhost:5432/airdress"  # omit for embedded
# data_dir = "./var/postgres"
# version = "=18.3.0"
# shared_buffers_mb = 64
# max_connections = 32
# pool_max_size = 16
# pool_acquire_timeout_secs = 5

[enrollment]
bootstrap_token = "change-me-on-first-run"
# operator_base_url = "http://127.0.0.1:8080"

[logging]
# level = "info"
# format = "pretty"    # pretty | compact | json
# target = "stderr"
# debug_file = "/var/log/airdress/debug.log"
# service_name = "airdress-operator"
# service_version = "0.x.y"
# deployment_environment = "production"

[proxy]
enabled = true
# bind_http = "0.0.0.0:8081"
# via_token = "secret-proxy-token"
# rate_limit_rps = 100
# rate_limit_burst = 20

[proxy.health]
# enabled = true
# interval_secs = 5
# timeout_secs = 1
# unhealthy_threshold = 3
# healthy_threshold = 2

[proxy.circuit_breaker]
# enabled = true
# failure_threshold = 5
# success_threshold = 1
# cool_down_secs = 10

[chat]
enabled = true
# store = "postgres"   # memory | postgres

[ingress]
enabled = true
# buffer_size = 100
# metrics_interval_secs = 30

[[ingress.endpoints]]
slug = "github-webhook"
url = "http://localhost:3000/webhooks/github"

[apps]
enabled = true
# apps_dirs = ["/etc/airdress/apps"]
# workdir = "./var/apps"
# zone = "humans.airdress.co"

[miniapps]
enabled = true
# zone = "humans.airdress.co"
# bundle_root = "./var/bundles"
# max_bundle_size = 52428800
# render_cache_assets = true
# render_cache_ttl_secs = 3600
# miniapps_dirs = ["/etc/airdress/miniapps"]

[plugin_db.secrets]
# key_file = "/etc/airdress/secrets.key"
# Or set AIRDRESS_SECRETS_KEY env var with base64-encoded key
```

## Top-level fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `bind` | string | `"127.0.0.1:8080"` | HTTP listener address and port |
| `wg_port` | integer | `51820` | WireGuard UDP port |
| `name` | string | `"airdress-operator"` | Instance name, included in log output |

## `[dns]`

Built-in authoritative DNS server. When enabled, the operator answers DNS queries for records defined in the config or added at runtime via `airdress-operator dns add`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable the DNS server |
| `bind_udp` | string | `"0.0.0.0:53"` | UDP listener address |
| `bind_tcp` | string | `"0.0.0.0:53"` | TCP listener address |
| `zone` | string | `"humans.airdress.co"` | Zone name for served records |

### `[dns.soa]`

SOA record fields for the zone.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `mname` | string | | Primary nameserver hostname |
| `rname` | string | | Hostmaster email (dot-encoded) |

### `[dns.cache]`

In-memory record cache.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_entries` | integer | `10000` | Maximum cached entries |
| `default_ttl` | integer | `60` | Default TTL in seconds for records without an explicit TTL |

### `[dns.ratelimit]`

Response Rate Limiting to mitigate DNS amplification.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `queries_per_second` | integer | `1000` | Sustained query rate per source IP |
| `burst` | integer | `200` | Burst allowance above the sustained rate |

### `[[dns.records]]`

Static DNS records loaded at startup. Each entry is a table in the TOML array.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | | Fully qualified record name |
| `type` | string | | Record type: `A`, `AAAA`, `CNAME`, `TXT`, `MX`, `SRV` |
| `rdata` | string | | Record data |
| `ttl` | integer | `60` | Time to live in seconds |

## `[auth]`

OIDC-based authentication for the operator API.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `issuer` | string | | OIDC issuer URL |
| `jwks_uri` | string | | JWKS endpoint for token verification |
| `audience` | string | | Expected JWT `aud` claim |

**Tip:** Environment variable mapping uses double underscores for nested fields: `AIRDRESS_AUTH__ISSUER`, `AIRDRESS_AUTH__JWKS_URI`, `AIRDRESS_AUTH__AUDIENCE`.

## `[database]`

PostgreSQL connection and tuning. When `url` is omitted, the operator runs an embedded PostgreSQL child process using the settings below. When `url` is set, the operator connects to the external database and ignores `data_dir`, `version`, `shared_buffers_mb`, and `max_connections`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `url` | string | (none) | External PostgreSQL connection URL. Omit for embedded mode. |
| `data_dir` | string | `"./var/postgres"` | Data directory for embedded PostgreSQL |
| `version` | string | `"=18.3.0"` | Required PostgreSQL version (embedded mode) |
| `shared_buffers_mb` | integer | `64` | Shared buffer size in MB (embedded mode) |
| `max_connections` | integer | `32` | Max connections (embedded mode) |
| `pool_max_size` | integer | `16` | Connection pool maximum size |
| `pool_acquire_timeout_secs` | integer | `5` | Timeout in seconds when acquiring a pool connection |

### Embedded vs external PostgreSQL

The operator supports two database modes:

**Embedded (default)** -- when `database.url` is absent, the operator downloads and manages a PostgreSQL child process. Data is stored in `data_dir`. This is the simplest setup for single-node deployments and local development.

**External** -- when `database.url` is set (e.g. `postgresql://user:pass@db.example.com:5432/airdress`), the operator connects via `sqlx::PgPool`. Use this for production deployments where you want a managed database, replication, or shared state between operator instances.

```toml
# Embedded (just omit url)
[database]
data_dir = "/var/lib/airdress/postgres"

# External
[database]
url = "postgresql://airdress:secret@db.internal:5432/airdress"
pool_max_size = 32
```

The `AIRDRESS_DATABASE_URL` environment variable is the most common way to provide the connection string without writing it to the config file.

## `[enrollment]`

Device enrollment settings.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `bootstrap_token` | string | | Secret token for the first device enrollment |
| `operator_base_url` | string | | Base URL the operator advertises to enrolling devices |

**Caution:** Change the `bootstrap_token` from its initial value immediately after first enrollment. It is a shared secret that grants enrollment access.

## `[logging]`

Structured logging configuration.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `level` | string | `"info"` | Log level: `trace`, `debug`, `info`, `warn`, `error` |
| `format` | string | `"pretty"` | Output format: `pretty`, `compact`, or `json` |
| `target` | string | `"stderr"` | Log target |
| `debug_file` | string | (none) | Path to an additional debug log file |
| `service_name` | string | `"airdress-operator"` | Service name in structured log fields |
| `service_version` | string | | Version string in structured log fields |
| `deployment_environment` | string | | Environment tag (e.g. `production`, `staging`) |

## `[proxy]`

Reverse proxy for forwarding traffic to local services.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable the reverse proxy |
| `bind_http` | string | | HTTP listener for proxied traffic |
| `via_token` | string | | Token for the `Via` header (upstream authentication) |
| `rate_limit_rps` | integer | `100` | Requests per second per client |
| `rate_limit_burst` | integer | `20` | Burst allowance above the sustained rate |

### `[proxy.health]`

Health checking for proxy backends.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `true` | Enable backend health checks |
| `interval_secs` | integer | `5` | Seconds between health probes |
| `timeout_secs` | integer | `1` | Probe timeout in seconds |
| `unhealthy_threshold` | integer | `3` | Consecutive failures to mark unhealthy |
| `healthy_threshold` | integer | `2` | Consecutive successes to mark healthy |

### `[proxy.circuit_breaker]`

Circuit breaker for proxy backends. When a backend exceeds the failure threshold, the circuit opens and requests are rejected immediately until the cool-down period expires.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `true` | Enable the circuit breaker |
| `failure_threshold` | integer | `5` | Failures before the circuit opens |
| `success_threshold` | integer | `1` | Successes during half-open to close the circuit |
| `cool_down_secs` | integer | `10` | Seconds before retrying after the circuit opens |

## `[chat]`

Chat subsystem.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable the chat subsystem |
| `store` | string | `"memory"` | Message store backend: `memory` or `postgres` |

The chat section also supports `limits`, `sse`, and `federation` subsections for advanced tuning. Federation is feature-gated and may not be available in all builds.

## `[ingress]`

Inbound event endpoints (webhooks, callbacks).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable inbound event processing |
| `buffer_size` | integer | `100` | In-memory event buffer size |
| `metrics_interval_secs` | integer | `30` | Interval for emitting ingress metrics |

### `[[ingress.endpoints]]`

Each entry defines a named inbound endpoint that receives events and forwards them to a local URL.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `slug` | string | | URL-safe endpoint identifier |
| `url` | string | | Local URL to forward events to |

## `[apps]`

Mini-app runtime.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable the app runtime |
| `apps_dirs` | string[] | | Directories to scan for app definitions |
| `workdir` | string | `"./var/apps"` | Working directory for app data |
| `zone` | string | | DNS zone for app subdomains |

## `[miniapps]`

Mini-app serving and registry.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable mini-app serving |
| `zone` | string | | DNS zone for mini-app subdomains |
| `bundle_root` | string | `"./var/bundles"` | Directory for downloaded bundles |
| `max_bundle_size` | integer | `52428800` | Maximum bundle size in bytes (default 50 MB) |
| `render_cache_assets` | boolean | `true` | Cache rendered assets |
| `render_cache_ttl_secs` | integer | `3600` | Asset cache TTL in seconds |
| `miniapps_dirs` | string[] | | Directories to scan for mini-app definitions |

The miniapps section also supports `registry`, `tokens`, `capability`, and `upgrade` subsections for registry authentication, capability grants, and upgrade policies.

## `[plugin_db]`

Encrypted storage for plugin secrets.

### `[plugin_db.secrets]`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `key_file` | string | (none) | Path to a file containing the base64-encoded 32-byte master key |

Alternatively, set the `AIRDRESS_SECRETS_KEY` environment variable with the base64-encoded key directly. Generate a key with `airdress-operator plugin-db init-key`.

## Environment variable mapping

Config fields map to environment variables with the `AIRDRESS_` prefix. Nested sections use double underscores as separators.

| Config path | Environment variable |
|-------------|---------------------|
| `bind` | `AIRDRESS_BIND` |
| `wg_port` | `AIRDRESS_WG_PORT` |
| `database.url` | `AIRDRESS_DATABASE_URL` |
| `auth.issuer` | `AIRDRESS_AUTH__ISSUER` |
| `auth.jwks_uri` | `AIRDRESS_AUTH__JWKS_URI` |
| `auth.audience` | `AIRDRESS_AUTH__AUDIENCE` |
| `enrollment.bootstrap_token` | `AIRDRESS_ENROLLMENT__BOOTSTRAP_TOKEN` |
| `logging.level` | `AIRDRESS_LOG` |
| `chat.federation.keystore_passphrase` | `AIRDRESS_CHAT__FEDERATION__KEYSTORE_PASSPHRASE` |
| `plugin_db.secrets` (key value) | `AIRDRESS_SECRETS_KEY` |

## See also

[CLI Reference](/reference/cli/)
  [Quickstart](/getting-started/quickstart/)
  [Deployment Checklist](/guides/deployment-checklist/)