9.8 KiB
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 TOML parser. Stripped
binary is roughly 6 MB.
Quick Start
# 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; the
production deploy guide is in 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, andgeneric. Each gets its own endpoint, rate limit, CORS allowlist, and honeypot field. - SMTP delivery — Go stdlib
net/smtpwithPLAINauth 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.Validateis reusable; validates name, email (RFC 5322), service allow-list (forcontact), and message length. - Reusable Go package —
pkg/contactformexports typedRequest,Response, andErrorResponsestructs plus theValidatefunction you can import into any Go project. - Structured logging —
log/slogwith 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
interpreslibrary. Unknown fields and unknown form types are rejected at startup so typos fail loudly. - Environment variable expansion —
${VAR_NAME}and$VAR_NAMEin 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.
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.
Form Types
| Type | Required fields | Email subject | Stores to disk |
|---|---|---|---|
contact |
name, email, message; optional service |
[nuntius/<name>] Contact form submission |
|
feedback |
name, email, message |
[nuntius/<name>] New feedback |
|
newsletter |
email |
[nuntius/<name>] New newsletter subscriber |
✓ |
generic |
name, email, message |
[nuntius/<name>] 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.
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_originslist. Requests from disallowed origins receive a403 Forbidden. - Rate limiting — token-bucket algorithm, per IP, per form, configurable
submissions per hour. In-memory; no Redis required. Set
rate_limit_per_hourto0to disable for a given form. - Server timeouts —
ReadHeaderTimeoutprevents 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. 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
nuntiussystem user and group - The binary at
/usr/local/bin/nuntius - Config auto-generation at
/etc/nuntius/config.toml - Environment file at
/etc/nuntius/.env(mode0600) - 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.
Library Usage
The pkg/contactform package is a small, dependency-free Go library you can
import into any project:
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.
Development
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
interpreslibrary; newsletter storage is an append-only JSONL file viabufio; rate limiting is in-memory.
Documentation
docs/architecture.md— layers, request flow, design decisionsdocs/configuration.md— full config schema, form types, secrets, newsletter logdocs/api-reference.md— HTTP verbs, status codes,GET /healthdocs/deployment.md— production systemd, Caddy, updatesdocs/security.md— threat model, CORS, rate limiting, secret handlingdocs/library-usage.md—pkg/contactformAPI and standalone usage
Contributing
See CONTRIBUTING.md for development setup, code style, commit
conventions, and the pull request flow.
License
MIT — see LICENSE.
Copyright © 2026 Petr Balvín