# Architecture How nuntius is put together. Every node, package and arrow below exists in the source tree; nothing is aspirational. ## Overview ```mermaid flowchart TD Browser[Browser / Frontend] --> Caddy[Caddy reverse proxy] Caddy --> CLI[cmd/server/main.go] CLI --> Config[internal/config] CLI --> Handler[internal/handler] Handler --> Validate[internal/contactform] Handler --> Email[internal/email] Handler --> Telegram[internal/telegram] Handler --> Storage[internal/storage] Email -->|SMTP PLAIN| SMTP[(SMTP server)] Telegram -->|Bot API| Chat[(Telegram chat)] Storage -->|append JSONL| Disk[(data_dir)] ``` One process serves many forms. The entry point loads the configuration and builds a `ContactHandler`; every request then moves through one pipeline: CORS, rate limit, parse, honeypot, validate, deliver, persist. The layers below the handler know nothing about HTTP, and the handler knows nothing about SMTP or the Bot API beyond two small interfaces. ## Packages | Package | Responsibility | |---|---| | `cmd/server` | Orchestration only: slog setup, config load, handler build, route registration, request-log middleware, the `http.Server` with its configured timeouts, and the graceful shutdown on SIGINT or SIGTERM. No business logic. | | `internal/config` | The gatekeeper for everything that varies per deployment. `${VAR}` expansion runs on the raw text before `interpres` parses it, unknown fields are rejected, and `validate()` enforces required fields, unique static paths, known form types and sane policy values. `(*Form).Policy()` resolves a form's validation policy: the built-in preset of its type with the configured `services`, `require_*` and rune-limit keys applied on top. Optional keys are pointers, so an omitted key falls back to its default while an explicit zero keeps its documented meaning. | | `internal/contactform` | The request types and the one validator in the codebase, `Validate(req, policy)`, plus `Preset(formType)` and the `NormalizeAndValidate` preset wrapper. No awareness of HTTP, SMTP, the filesystem, or the platform. | | `internal/handler` | The HTTP pipeline: per-form routes, CORS, rate limiting, honeypot, delivery fan-out, double opt-in confirmation, metrics registry, and the state persistence across restarts. It owns no delivery or storage logic; it calls the interfaces below. | | `internal/email` | MIME composition (multipart/alternative, per-type templates, sanitised headers, randomised boundary) and deadline-bounded SMTP delivery with per-form credentials. STARTTLS, implicit TLS on port 465 and the `require_tls` policy are the operator's transport choices, enforced in code. | | `internal/telegram` | One-way submission summaries through the Bot API, plain text, the bot token redacted from every error. | | `internal/storage` | The append-only JSONL stores: newsletter subscribers with a dedupe index, pending double opt-in tokens, and the optional submission archive. Malformed lines are skipped on read, never fatal. | | `internal/version` | The release identity read from the build information; nothing is injected. | A new form type is a preset in `contactform.Preset`, a subject and a `compose` branch in `internal/email`, and the wiring in `internal/config`; every other layer is type-agnostic. ## Data flow ```mermaid sequenceDiagram participant Client participant Handler as ContactHandler participant Limiter as rateLimiter participant Validate as contactform.Validate participant Sender as email.FormSender participant Bell as telegram.Notifier participant Store as storage Client->>Handler: POST