Haven Notify Logo
## Overview Haven Notify is an internal Go service that validates notification requests, renders Discord-compatible payloads, and delivers them to a configured Discord webhook. It runs as a standalone binary or container. Templates are embedded in the binary and validated at startup. ## Prerequisites - Go 1.25.10 - A Docker API compatible with Testcontainers for the end-to-end suite - A Discord webhook URL for normal service operation `WEBHOOK_URL` is required and must be an absolute HTTP or HTTPS URL. The process exits before becoming ready when the configuration or embedded templates are invalid. ## API All notification endpoints require `POST` and `Content-Type: application/json`. Request bodies are limited to 64 KiB, documented fields are required, and unknown fields are accepted for forward compatibility. ### V1 API The original routes, request bodies, and Discord message appearance remain unchanged. #### Send Notification - **Endpoint**: `/notify` - **Request Body**: ```json { "title": "Notification Title", "message": "Notification Message" } ``` #### Send Backup Notification - **Endpoint**: `/template/notify/backup` - **Request Body**: ```json { "title": "Nightly backup", "asset": "Photos", "backupSizeInMB": 1536, "extra": [ { "name": "Host", "value": "nebula" } ] } ``` #### Send Update Notification - **Endpoint**: `/template/notify/update` - **Request Body**: ```json { "host": "nexus", "asset": "k3s", "time": 42 } ``` `time` is expressed in seconds. #### Send Error Notification - **Endpoint**: `/template/notify/error` - **Request Body**: ```json { "caller": "backup-job", "message": "Error while moving file", "critical": true, "extra": [ { "name": "Path", "value": "/mnt/backups" } ] } ``` ### V2 API V2 notifications use compact, consistently styled Discord embeds. Colors, icons, timestamps, field ordering, and layout are controlled by Haven Notify rather than the caller. Every v2 request may include: - `source`: a nonblank string identifying the caller. - `occurredAt`: an RFC3339 timestamp. When omitted, Haven Notify uses the current UTC time. - `extra`: an array of nonblank `name` and `value` strings. Short single-line values render inline; multiline or long values render full-width. V2 preserves caller field order. Fixed notification fields are placed before `extra` fields so that trailing extras are discarded first if Discord's 25-field or 6,000-character embed limits are reached. Oversized text is safely truncated and extra fields may be omitted from the delivered message. #### Send a Message - **Endpoint**: `/api/v2/messages` - **Required**: `message` - **Optional**: `title`, `source`, `occurredAt`, `extra` ```json { "title": "Maintenance complete", "message": "The storage job completed successfully.", "source": "scheduler", "extra": [ { "name": "Region", "value": "home" } ] } ``` #### Send a Backup Notification - **Endpoint**: `/api/v2/notifications/backup` - **Required**: `asset`, nonnegative integer `sizeBytes` - **Optional**: `title`, `source`, nonnegative `durationSeconds`, `occurredAt`, `extra` ```json { "title": "Nightly backup", "asset": "Photos", "sizeBytes": 1610612736, "durationSeconds": 125, "source": "restic", "extra": [ { "name": "Host", "value": "nebula" } ] } ``` `sizeBytes` is displayed using IEC units such as KiB, MiB, and GiB. Durations use a compact form such as `42.5s`, `2m 5s`, or `1h 2m 3s`. #### Send an Update Notification - **Endpoint**: `/api/v2/notifications/update` - **Required**: `host`, `asset`, nonnegative `durationSeconds` - **Optional**: `source`, `fromVersion`, `toVersion`, `occurredAt`, `extra` ```json { "host": "nexus", "asset": "k3s", "durationSeconds": 42.5, "fromVersion": "1.32.5", "toVersion": "1.33.1", "source": "system-updater" } ``` When both versions are supplied, the embed displays a transition such as `1.32.5 → 1.33.1`. A single supplied version is labeled as the previous or installed version. #### Send an Error Notification - **Endpoint**: `/api/v2/notifications/error` - **Required**: `source`, `message`, `severity` - **Optional**: `errorCode`, `occurredAt`, `extra` ```json { "source": "backup-job", "message": "The repository is unavailable.", "severity": "critical", "errorCode": "E_REPOSITORY", "extra": [ { "name": "Path", "value": "/mnt/backups" } ] } ``` `severity` must be `warning`, `error`, or `critical`; it selects the embed title, icon, and color. ### Health Probes - `GET` or `HEAD /ready` returns `200 Ready` after configuration and templates initialize. - `GET` or `HEAD /live` returns `200 Alive` while the process can serve HTTP. ### Response Statuses | Status | Meaning | |---|---| | `200` | Notification delivered | | `400` | Invalid or missing request data | | `404` | Unknown route or template | | `405` | Unsupported method | | `415` | Content type is not JSON | | `502` | Discord rejected the request or delivery retries failed | | `504` | Discord delivery exceeded its timeout | Haven Notify suppresses Discord mention parsing and safely truncates content to Discord's message and embed limits. Transport errors, rate limits, and server errors receive at most two retries within a ten-second total delivery budget. ## Run ### Go ```sh WEBHOOK_URL=https://discord.com/api/webhooks/... go run . ``` ### Docker ```sh docker build -t haven-notify . docker run --rm -p 8080:8080 \ -e WEBHOOK_URL=https://discord.com/api/webhooks/... \ haven-notify ``` The runtime container uses a non-root `haven-notify` user and contains only the binary, Alpine runtime files, and CA certificates. ## Tests Run unit and HTTP contract tests: ```sh go test ./... ``` Run static analysis, the race detector, and coverage (the race detector requires CGO support): ```sh go vet ./... go test -race -shuffle=on -count=1 -covermode=atomic -coverprofile=coverage.out ./... go tool cover -func=coverage.out ``` Run the seeded fuzz target: ```sh go test -run=^$ -fuzz=FuzzErrorNotificationHandler -fuzztime=10s . ``` Run the black-box container suite: ```sh go test -tags=e2e -run '^TestContainerE2E$' -count=1 -timeout=5m -v . ``` The end-to-end suite builds the production Dockerfile, starts Haven Notify and a disposable MockServer on an isolated network, and verifies the complete HTTP-to-Discord payload flow. It never calls a real Discord webhook. Configure `DOCKER_HOST` or the Testcontainers host settings when the Docker daemon is not available through the platform's default socket. ## Deployment The Kubernetes Deployment, Service, and Ingress manifest lives in the `haven` homelab repository at `infra/haven-notify.yaml`. This repository holds the service source, Dockerfile, tests, and Gitea Actions pipeline. The production manifest maps the `HAVEN_WEBHOOK_URL` key from the `discord-webhook` secret into the container's `WEBHOOK_URL` environment variable.