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:
{
"title": "Notification Title",
"message": "Notification Message"
}
Send Backup Notification
- Endpoint:
/template/notify/backup - Request Body:
{
"title": "Nightly backup",
"asset": "Photos",
"backupSizeInMB": 1536,
"extra": [
{
"name": "Host",
"value": "nebula"
}
]
}
Send Update Notification
- Endpoint:
/template/notify/update - Request Body:
{
"host": "nexus",
"asset": "k3s",
"time": 42
}
time is expressed in seconds.
Send Error Notification
- Endpoint:
/template/notify/error - Request Body:
{
"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 nonblanknameandvaluestrings. 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
{
"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 integersizeBytes - Optional:
title,source, nonnegativedurationSeconds,occurredAt,extra
{
"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, nonnegativedurationSeconds - Optional:
source,fromVersion,toVersion,occurredAt,extra
{
"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
{
"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
GETorHEAD /readyreturns200 Readyafter configuration and templates initialize.GETorHEAD /livereturns200 Alivewhile 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
WEBHOOK_URL=https://discord.com/api/webhooks/... go run .
Docker
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:
go test ./...
Run static analysis, the race detector, and coverage (the race detector requires CGO support):
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:
go test -run=^$ -fuzz=FuzzErrorNotificationHandler -fuzztime=10s .
Run the black-box container suite:
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.
