Files
nuntius/docs/configuration.md

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_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.
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.