# Configuration Reference All runtime configuration for nuntius lives in a single TOML file. The default path is `/etc/nuntius/config.toml`; if the file does not exist when the server starts, nuntius writes a three-form template there. The parser rejects unknown fields and unknown form types so typos fail loudly at startup instead of silently disabling a form. ## Quick Reference ```toml data_dir = "./data" [server] port = 8080 [[forms]] name = "contact" type = "contact" path = "/api/nuntius/contact" to = "you@example.com" from = "noreply@example.com" rate_limit_per_hour = 10 honeypot_field = "website" allowed_origins = ["https://example.com"] [forms.smtp] host = "smtp.example.com" port = 587 user = "noreply@example.com" password = "${NUNTIUS_SMTP_PASSWORD}" ``` ## Top-Level Fields | Field | Type | Required | Default | Description | |---|---|:---:|---|---| | `server` | table | | `{"port": 8080}` | Server-wide settings | | `server.port` | int | | `8080` | HTTP listen port | | `data_dir` | string | | `"./data"` | Directory for newsletter JSONL logs; created on first write | | `forms` | array | ✅ | — | One or more form definitions (must contain at least one) | > **Note:** Path resolution for `data_dir` is relative to the process's current working directory. The systemd unit installed in `docs/deployment.md` sets `WorkingDirectory=/var/lib/nuntius`, so the default `data_dir: "./data"` resolves to `/var/lib/nuntius/data/` in production. ## Form Fields | Field | Type | Required | Default | Description | |---|---|:---:|---|---| | `name` | string | ✅ | — | Internal identifier. Appears in logs and in `data_dir/newsletter-.jsonl`. | | `path` | string | ✅ | — | HTTP path the form is served on (e.g. `/api/nuntius/contact`). Must start with `/`. Must be unique across all forms. | | `type` | string | | `"contact"` | One of `contact`, `feedback`, `newsletter`, `generic`. See [Form types](#form-types). | | `smtp` | table | ✅ | — | SMTP connection settings. See [SMTP fields](#smtp-fields). | | `to` | string | ✅ | — | Recipient address (`To:` header). | | `from` | string | | `smtp.user` | Sender address (`From:` header). | | `rate_limit_per_hour` | int | | `10` | Submissions per IP per hour. `0` disables. | | `honeypot_field` | string | | `"website"` | Name of the invisible form field. Empty string disables the honeypot. | | `allowed_origins` | []string | | `[]` | CORS allowlist. One entry per origin (e.g. `https://example.com`). | ## SMTP Fields | Field | Type | Required | Default | Description | |---|---|:---:|---|---| | `smtp.host` | string | ✅ | — | SMTP server hostname | | `smtp.port` | int | ✅ | — | SMTP port; `587` for STARTTLS, `465` for implicit TLS (Go's `net/smtp` does both via `SendMail`) | | `smtp.user` | string | ✅ | — | SMTP auth user (typically the `from` address) | | `smtp.password` | string | ✅ | — | SMTP auth password. See [Secrets](#secrets-via-environment-variables). | ## Form Types | Type | Required request fields | Email subject | Notes | |---|---|---|---| | `contact` | `name`, `email`, `message`, optional `service` | `[nuntius/] Contact form submission`, or `[nuntius/][] ...` if `service` is set | The default. `service` is restricted to an allow-list: `architecture`, `ai`, `infrastructure`, `software`, `unix`, `other`. | | `feedback` | `name`, `email`, `message` | `[nuntius/] New feedback` | Same shape as `contact` minus the `service` field. Distinct HTML template. | | `newsletter` | `email` | `[nuntius/] New newsletter subscriber` | Also writes the email to disk as a JSONL line in `data_dir/newsletter-.jsonl`. No double opt-in. | | `generic` | `name`, `email`, `message` | `[nuntius/] New submission` | Escape hatch for one-off forms that do not fit the other three. | All four types are rate-limited, CORS-checked, and honeypot-protected **independently** — each form has its own per-IP bucket, allowlist, and honeypot field. `newsletter` is the only type that writes to disk; the other three only send mail. ### Service allow-list (for `contact`) The `service` field on `contact` is restricted to a fixed list (defined in `pkg/contactform/validate.go`): | Value | Use | |---|---| | `architecture` | Architecture / system design | | `ai` | AI / ML engagements | | `infrastructure` | Infrastructure / DevOps / SRE | | `software` | Software engineering | | `unix` | Unix / Linux tooling | | `other` | Anything else | An empty string is also valid (skip the field). Anything else triggers a `400 validation` error with `details[0].field = "service"`. To add a new value, edit `validServices` in `pkg/contactform/validate.go` and update this table. ## Validation Rules The `pkg/contactform.Validate` function enforces the following limits (in runes, not bytes): | Field | Min | Max | Validator | |---|:---:|:---:|---| | `name` | 2 | 100 | `utf8.RuneCountInString` after `strings.TrimSpace` | | `email` | — | — | `net/mail.ParseAddress` (RFC 5322) after `strings.TrimSpace` | | `service` | — | — | allow-list (see above) | | `message` | 10 | 5,000 | `utf8.RuneCountInString` after `strings.TrimSpace` | Whitespace is trimmed from `name`, `email`, `service`, and `message` before validation. The trimmed values are what the email body sees. ## Newsletter Subscribers For every accepted `newsletter` submission, nuntius appends one JSON line to `data_dir/newsletter-.jsonl`: ```json {"email":"jane@example.com","ip":"203.0.113.42","form":"newsletter","created_at":"2026-06-16T12:34:56Z"} ``` | Field | Description | |---|---| | `email` | Trimmed, lowercased-on-output by your downstream tool — nuntius does not normalize | | `ip` | Client IP as seen by nuntius (honours `X-Forwarded-For` / `X-Real-IP`) | | `form` | The `name` of the form the signup hit | | `created_at` | UTC timestamp set by nuntius at `Append` time | The log is **append-only** and **survives restarts**. It is **not rotated automatically** — add a `logrotate` unit (or equivalent) if it grows. Malformed lines are skipped on read, so a partial write can never brick the file. Quick shell recipes: ```bash # Count subscribers wc -l /var/lib/nuntius/data/newsletter-newsletter.jsonl # Extract just the emails jq -r .email /var/lib/nuntius/data/newsletter-newsletter.jsonl # Extract unique emails sorted jq -r .email /var/lib/nuntius/data/newsletter-newsletter.jsonl | sort -u # Signups per day jq -r .created_at[:10] /var/lib/nuntius/data/newsletter-newsletter.jsonl | sort | uniq -c ``` ## Secrets via Environment Variables The TOML file supports `${VAR_NAME}` and `$VAR_NAME` expansion **before parsing**, so secrets can stay out of the file. Example: ```toml [forms.smtp] host = "smtp.example.com" port = 587 user = "noreply@example.com" password = "${NUNTIUS_SMTP_PASSWORD}" ``` The `NUNTIUS_SMTP_PASSWORD` env var is read at startup and substituted into the TOML before it is parsed. If the env var is unset or empty, an empty string is substituted — so make sure the variable is set in the process environment (systemd `EnvironmentFile=`, container secrets, etc.). SMTP credentials are never logged. Recommended production setup: ```bash # /etc/nuntius/.env (mode 0600, owner root:nuntius) NUNTIUS_SMTP_PASSWORD=actual-app-password ``` ```ini # /etc/systemd/system/nuntius.service [Service] EnvironmentFile=/etc/nuntius/.env ``` The `install.py` helper script sets the file mode automatically. ## Defaults at a Glance When a key is missing from the TOML, nuntius applies the following defaults: | Key | Default | Source | |---|---|---| | `server.port` | `8080` | `(*Config).validate()` | | `data_dir` | `"./data"` | `(*Config).validate()` | | `forms[i].type` | `"contact"` | `(*Config).validate()` | | `forms[i].from` | `forms[i].smtp.user` | `(*Config).validate()` | | `forms[i].rate_limit_per_hour` | `10` | `(*Config).validate()` | | `forms[i].honeypot_field` | `"website"` | `(*Config).validate()` | Setting `rate_limit_per_hour` to `0` explicitly disables rate limiting for that form (use with care — there is no global safety net). ## Validation Errors at Startup If the config file is malformed, nuntius exits 1 and writes a single JSON line to stderr. Examples: ```text {"time":"...","level":"ERROR","msg":"config load failed","path":"/etc/nuntius/config.toml","err":"parse /etc/nuntius/config.toml: interpres: unknown field \"smtp_ost\""} {"time":"...","level":"ERROR","msg":"config load failed","path":"/etc/nuntius/config.toml","err":"form \"contact\": type \"contatc\" is not supported (must be one of: contact, feedback, newsletter, generic)"} {"time":"...","level":"ERROR","msg":"config load failed","path":"/etc/nuntius/config.toml","err":"form \"contact\": path must start with /"} {"time":"...","level":"ERROR","msg":"config load failed","path":"/etc/nuntius/config.toml","err":"form \"feedback\": duplicate path \"/api/nuntius/contact\" (also used by \"contact\")"} ``` Fix the reported line and re-run; the server will not start with a broken config.