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 nginx on a tiny Linux server. Only the first-party interpres TOML module, no external third-party Go modules, ~6 MB binary.
Features
- Single static binary —
go build -ldflags="-s -w"produces ≈ 6 MB; no Node, no Python, no Redis, no Postgres, no third-party Go packages. - Four form kinds out of the box —
contact,feedback,newsletter, andgeneric. Each gets its own endpoint, rate limit, CORS allowlist, and honeypot field. - SMTP delivery — Go stdlib
net/smtpwithPLAINauth. Works with any provider that exposes SMTP. - 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 signup. - Server-side validation —
pkg/contactform.Validateis reusable; name, email (RFC 5322), service allow-list (forcontact), and message length. - Reusable package —
pkg/contactformexports typedRequest/Response/ErrorResponseplus aValidatefunction you can drop into any Go project. - Structured logging —
log/slogwith JSON output to stdout; plays well with journald, Loki, Datadog… - Graceful shutdown — SIGINT/SIGTERM close in-flight requests within 15 s.
- Rate limiting — token-bucket per IP, configurable per hour, in-memory (no Redis required).
- Honeypot field — invisible to humans, required for spam bots. Silently accepts and drops.
- Config validation — unknown fields and unknown form types are rejected at startup, so typos fail loudly instead of silently disabling a form.
- Env-var secrets —
${VAR_NAME}/$VAR_NAMEexpansion in the TOML file keeps SMTP passwords out of disk.
Quick Start
# 1. Clone
git clone https://sourcedock.dev/petrbalvin/nuntius.git
cd nuntius
# 2. Build
just build
# → ./bin/nuntius
# 3. Run (auto-creates /etc/nuntius/config.toml with a 3-form template on first start)
sudo mkdir -p /etc/nuntius
just run
# → "nuntius starting" addr=:8080 forms=3
# 4. Edit the generated config to point at your SMTP and recipient
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.
The full schema lives in docs/configuration.md; the deploy guide is in docs/deployment.md.
Usage
Test the endpoints
# Contact form
curl -X POST http://localhost:8080/api/nuntius/contact \
-H "Content-Type: application/json" \
-H "Origin: https://petrbalvin.org" \
-d '{
"name": "Jane Doe",
"email": "jane@example.com",
"service": "architecture",
"message": "Hello, I would like to discuss an engagement."
}'
# Feedback (no service field)
curl -X POST http://localhost:8080/api/nuntius/feedback \
-H "Content-Type: application/json" \
-H "Origin: https://petrbalvin.org" \
-d '{
"name": "Jane Doe",
"email": "jane@example.com",
"message": "I love how the site uses Vue.js with zero runtime deps."
}'
# Newsletter (email only)
curl -X POST http://localhost:8080/api/nuntius/newsletter \
-H "Content-Type: application/json" \
-H "Origin: https://petrbalvin.org" \
-d '{ "email": "jane@example.com" }'
Successful response (all three):
{ "ok": true }
Validation error (e.g. newsletter with bad email):
{
"error": "validation",
"details": [
{ "field": "email", "message": "email is invalid" }
]
}
Frontend integration
If you proxy /api/nuntius/* through nginx (recommended, see docs/deployment.md), no frontend config is needed — the form posts to the same origin as the page. Otherwise, set VITE_NUNTIUS_URL to the upstream origin:
await fetch(`${import.meta.env.VITE_NUNTIUS_URL || ''}/api/nuntius/contact`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name.value,
email: email.value,
service: service.value,
message: message.value,
}),
});
Use the package in your own project
pkg/contactform is a small, dependency-free Go package that 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!",
}
if errs := contactform.Validate(req, "contact"); len(errs) > 0 {
// handle validation errors
}
Full library example: docs/library-usage.md. Full HTTP API: docs/api-reference.md.
Configuration (TL;DR)
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}"
Form types and full schema: docs/configuration.md. Secrets via env vars, CORS, honeypot, rate limit, and validation rules are all documented there.
Requirements
- Go 1.26+ (uses Go 1.22+
http.ServeMuxmethod patterns andlog/slog) - An SMTP account (Yandex, Mailgun, Postmark, your own Postfix, …)
- No other runtime dependencies. No Node, no Python, no Redis, no Postgres. Configuration is TOML parsed via the first-party
interpreslibrary; newsletter storage is an append-only JSONL file viabufio.
Development
just install # go mod tidy + go mod download (no-op for this project, kept for symmetry)
just build # → ./bin/nuntius (with -s -w)
just test # go test ./...
just run # go run ./cmd/server
just uninstall # rm -rf ./bin
All four just recipes are mandatory per the project conventions and produce zero errors and zero warnings.
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, nginx, updatesdocs/security.md— threat model, CORS, rate limiting, secret handlingdocs/library-usage.md—pkg/contactformAPI and standalone use
Contributing
See CONTRIBUTING.md for development setup, code style, commit conventions, and the pull request flow.
Security
For a full threat model, see docs/security.md. The short version: every form has its own per-IP rate limit, CORS allowlist, and honeypot field; SMTP credentials never touch the log; unknown config fields are rejected at startup.
If you find a security issue, please email opensource@petrbalvin.org rather than opening a public issue.
License
MIT — see LICENSE.
Copyright © 2026 Petr Balvín