Files
server-scripts/haven-notify
Jose Henrique 8db1db888f
All checks were successful
Check scripts syntax / check-scripts-syntax (push) Successful in 3s
Haven Notify Build and Deploy / Test Haven Notify (push) Successful in 46s
Haven Notify Build and Deploy / Build Haven Notify Image (PR) (push) Has been skipped
Haven Notify Build and Deploy / Build Haven Notify Image (push) Successful in 20s
Haven Notify Build and Deploy / Deploy Haven Notify (internal) (push) Successful in 3s
adding api/v2 endpoints
2026-07-16 09:51:53 -03:00
..
2025-10-06 22:14:42 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:51:53 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:51:53 -03:00
2026-07-16 09:51:53 -03:00
2026-07-16 09:51:53 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:14:51 -03:00
2026-07-16 09:51:53 -03:00
2026-07-16 09:51:53 -03:00

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:
{
  "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 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
{
  "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
{
  "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
{
  "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

  • 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

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.