8.9 KiB
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
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_diris relative to the process's current working directory. The systemd unit installed indocs/deployment.mdsetsWorkingDirectory=/var/lib/nuntius, so the defaultdata_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. |
|
smtp |
table | ✅ | — | SMTP connection settings. See 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. |
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:
{"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:
# 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:
[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:
# /etc/nuntius/.env (mode 0600, owner root:nuntius)
NUNTIUS_SMTP_PASSWORD=actual-app-password
# /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:
{"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.