# Nuntius — A Small, Opinionated Contact Form Backend > Latin *nuntius* = "messenger" — the service that carries messages from visitors to you. **nuntius** is a small, modular contact form backend written in Go. It serves multiple JSON endpoints (contact, feedback, newsletter, generic) over a single static binary, delivers submissions via SMTP, optionally persists newsletter signups to a JSONL log, and is designed to sit behind Caddy on a modest Linux or FreeBSD server. Module `sourcedock.dev/petrbalvin/nuntius` — Go 1.26, zero third-party dependencies. The only external module is the first-party [`interpres`](https://sourcedock.dev/petrbalvin/interpres) TOML parser. Stripped binary is roughly 6 MB. ## Quick Start ```bash # 1. Clone git clone https://sourcedock.dev/petrbalvin/nuntius.git cd nuntius # 2. Build just build # → ./bin/nuntius # 3. Run (writes a three-form config template to /etc/nuntius/config.toml on first start) sudo mkdir -p /etc/nuntius just run # → nuntius starting addr=[::]:8080 forms=3 # 4. Edit the generated config with your SMTP credentials and recipient address sudo $EDITOR /etc/nuntius/config.toml just run # restart to pick up changes ``` A default `config.toml` is auto-generated at `/etc/nuntius/config.toml` on first run with three sample forms (contact, feedback, newsletter). Edit it before exposing the service to the internet. The full schema lives in [`docs/configuration.md`](docs/configuration.md); the production deploy guide is in [`docs/deployment.md`](docs/deployment.md). ## Features - **Single static binary** — `go build -ldflags="-s -w"` produces roughly 6 MB; no Node, no Python, no Redis, no Postgres, no third-party Go packages. - **Four form kinds** — `contact`, `feedback`, `newsletter`, and `generic`. Each gets its own endpoint, rate limit, CORS allowlist, and honeypot field. - **SMTP delivery** — Go stdlib `net/smtp` with `PLAIN` auth over STARTTLS or implicit TLS. Works with any standards-compliant SMTP provider. - **Newsletter subscribers on disk** — append-only JSONL log under `data_dir/`. No database, no double opt-in. One `{email, ip, form, created_at}` line per valid signup. - **Server-side validation** — `pkg/contactform.Validate` is reusable; validates name, email (RFC 5322), service allow-list (for `contact`), and message length. - **Reusable Go package** — `pkg/contactform` exports typed `Request`, `Response`, and `ErrorResponse` structs plus the `Validate` function you can import into any Go project. - **Structured logging** — `log/slog` with JSON output to stdout; plays well with journald, Loki, Datadog, and similar. - **Graceful shutdown** — SIGINT and SIGTERM close in-flight requests within a configurable deadline (default 15 s). - **Token-bucket rate limiting** — per IP, per form, configurable submissions per hour; in-memory, no external store required. - **Honeypot field** — invisible to humans, required by bots. Silently accepts and drops spam submissions. - **TOML configuration** — single file, parsed by the first-party `interpres` library. Unknown fields and unknown form types are rejected at startup so typos fail loudly. - **Environment variable expansion** — `${VAR_NAME}` and `$VAR_NAME` in the TOML file keep SMTP credentials out of version control. - **IPv6-first** — binds to `[::]` by default, with automatic IPv4 compatibility via dual-stack sockets. - **Cross-platform** — pre-built binaries for Linux (amd64, arm64, riscv64, loong64) and FreeBSD (amd64, arm64, riscv64) on every release. ## Configuration Default path: `/etc/nuntius/config.toml`. A starter file is written automatically on first start. ```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}" ``` Secrets are injected via environment variables using `${VAR_NAME}` syntax. The full schema, form types, validation rules, and newsletter log format are documented in [`docs/configuration.md`](docs/configuration.md). ## Form Types | Type | Required fields | Email subject | Stores to disk | |------|-----------------|---------------|:---:| | `contact` | `name`, `email`, `message`; optional `service` | `[nuntius/] Contact form submission` | | | `feedback` | `name`, `email`, `message` | `[nuntius/] New feedback` | | | `newsletter` | `email` | `[nuntius/] New newsletter subscriber` | ✓ | | `generic` | `name`, `email`, `message` | `[nuntius/] New submission` | | 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 email. The `contact` form restricts the `service` field to an allow-list: `architecture`, `ai`, `infrastructure`, `software`, `unix`, `other`, or empty. Full form type documentation: [`docs/configuration.md#form-types`](docs/configuration.md#form-types). ## Security Every form endpoint is protected independently: - **Honeypot** — a configurable invisible form field silently drops submissions that contain it. Enabled per form via `honeypot_field`. - **CORS** — per-form `allowed_origins` list. Requests from disallowed origins receive a `403 Forbidden`. - **Rate limiting** — token-bucket algorithm, per IP, per form, configurable submissions per hour. In-memory; no Redis required. Set `rate_limit_per_hour` to `0` to disable for a given form. - **Server timeouts** — `ReadHeaderTimeout` prevents slow-loris attacks; graceful shutdown deadline ensures in-flight requests are drained before exit. - **Secret hygiene** — SMTP credentials are never logged; TOML values use `${VAR_NAME}` expansion so passwords stay in the environment, not on disk. - **Config validation** — unknown TOML fields and unknown form types cause a startup failure, preventing typo-driven misconfigurations from reaching production. - **Input validation** — server-side validation of all fields with configurable length limits; trimmed whitespace; RFC 5322 email parsing. For a full threat model and detailed security discussion, see [`docs/security.md`](docs/security.md). To report a vulnerability privately, email **opensource@petrbalvin.org**. ## Production Deployment Build the binary on your workstation, upload it with the systemd unit and environment template to your server, then run `python3 install.py` as root. The script is idempotent and sets up: - A dedicated `nuntius` system user and group - The binary at `/usr/local/bin/nuntius` - Config auto-generation at `/etc/nuntius/config.toml` - Environment file at `/etc/nuntius/.env` (mode `0600`) - A systemd service unit at `/etc/systemd/system/nuntius.service` - Data directory at `/var/lib/nuntius` The service is **enabled but not started** — you review the config and environment file first, then start it manually. Complete deployment instructions including Caddy reverse-proxy configuration, TLS setup, log rotation, and updates are in [`docs/deployment.md`](docs/deployment.md). ## Library Usage The `pkg/contactform` package is a small, dependency-free Go library you can import into any project: ```go import "sourcedock.dev/petrbalvin/nuntius/pkg/contactform" req := &contactform.Request{ Name: "Jane", Email: "jane@example.com", Service: "architecture", Message: "Hello, I would like to discuss an engagement.", } if errs := contactform.Validate(req, "contact"); len(errs) > 0 { // handle validation errors } ``` The package exports typed `Request`, `Response`, and `ErrorResponse` structs, the `Validate` function, and all form kind constants. A full standalone example is in [`docs/library-usage.md`](docs/library-usage.md). ## Development ```bash just install # go mod tidy + go mod download just build # → ./bin/nuntius (stripped, -s -w) just test # go test -race ./...; enforces ≥80% coverage on internal + pkg just fmt # gofmt -w cmd internal pkg just run # go run ./cmd/server ``` All `just` recipes produce zero errors and zero warnings. Tests include the race detector and enforce an 80% coverage floor on the `internal` and `pkg` packages. The CI pipeline (Gitea Actions, `.gitea/workflows/`) runs the full test suite and performs a multi-platform build matrix on every release tag (`linux/amd64`, `linux/arm64`, `linux/riscv64`, `linux/loong64`, `freebsd/amd64`, `freebsd/arm64`, `freebsd/riscv64`). ## Requirements - **Go 1.26+** - An **SMTP** account (any standards-compliant provider — Postfix, Yandex 360, Mailgun, Postmark, etc.) - **No other runtime dependencies.** No Node, no Python, no Redis, no Postgres. Config is TOML parsed by the first-party [`interpres`](https://sourcedock.dev/petrbalvin/interpres) library; newsletter storage is an append-only JSONL file via `bufio`; rate limiting is in-memory. ## Documentation - [`docs/architecture.md`](docs/architecture.md) — layers, request flow, design decisions - [`docs/configuration.md`](docs/configuration.md) — full config schema, form types, secrets, newsletter log - [`docs/api-reference.md`](docs/api-reference.md) — HTTP verbs, status codes, `GET /health` - [`docs/deployment.md`](docs/deployment.md) — production systemd, Caddy, updates - [`docs/security.md`](docs/security.md) — threat model, CORS, rate limiting, secret handling - [`docs/library-usage.md`](docs/library-usage.md) — `pkg/contactform` API and standalone usage ## Contributing See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, code style, commit conventions, and the pull request flow. ## License MIT — see [LICENSE](./LICENSE). Copyright © 2026 [Petr Balvín](https://petrbalvin.org)