Files
nuntius/docs/configuration.md
T

192 lines
8.9 KiB
Markdown
Raw Normal View History

# 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-<name>.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/<name>] Contact form submission`, or `[nuntius/<name>][<service>] ...` if `service` is set | The default. `service` is restricted to an allow-list: `architecture`, `ai`, `infrastructure`, `software`, `unix`, `other`. |
| `feedback` | `name`, `email`, `message` | `[nuntius/<name>] New feedback` | Same shape as `contact` minus the `service` field. Distinct HTML template. |
| `newsletter` | `email` | `[nuntius/<name>] New newsletter subscriber` | Also writes the email to disk as a JSONL line in `data_dir/newsletter-<name>.jsonl`. No double opt-in. |
| `generic` | `name`, `email`, `message` | `[nuntius/<name>] 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-<name>.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.sh` 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.