# alert-bridge `alert-bridge` turns an authenticated HTTP POST from an external caller (e.g. a cronjob running elsewhere on the home network) into a message posted to a Discord channel. It is fire-and-forget: each request becomes one new Discord message, there is no editing or reacting to prior messages. Unlike `ha-gateway`/`ai-gateway`/`discord-bot`/`tts-gateway`, it has no dependency on any other service in this repo — it posts directly to a Discord [Incoming Webhook](https://discord.com/developers/docs/resources/webhook), not through `discord-bot`'s bot session. That keeps the whole flow to one hop and means `discord-bot` needed zero changes to support this feature. ## Runtime Flow 1. The process loads `.env`, configures logging/telemetry, and starts serving HTTP on `HTTP_PORT`. 2. A caller `POST`s to `/alerts` with a bearer token and a JSON body. 3. The handler checks the bearer token (constant-time compare against `API_KEY`), validates the body, and defaults `level` to `info` if omitted. 4. The alert is formatted as a Discord embed (color-coded by level) and POSTed to `DISCORD_WEBHOOK_URL`. `level=error` alerts additionally prefix the message with a mention of `MENTION_USER_ID`, since embeds alone never trigger a Discord ping. ## HTTP API - `POST /alerts` — requires `Authorization: Bearer `. ```json {"source": "ba-cronjob", "message": "Finished with 0 errors", "level": "info"} ``` - `source`, `message`: required. - `level`: optional, one of `info` | `warn` | `error`, defaults to `info`. - Responses: `202` delivered, `400` bad request, `401` bad/missing token, `502` the Discord webhook call itself failed. - `GET /healthz`: unauthenticated liveness/readiness check, always `200 OK`. ## Configuration Environment variables: | Variable | Default | Description | | --- | --- | --- | | `HTTP_PORT` | `8080` | HTTP listen port | | `API_KEY` | required | Shared secret callers must present as `Authorization: Bearer ` | | `DISCORD_WEBHOOK_URL` | required | Target channel's Discord Incoming Webhook URL | | `MENTION_USER_ID` | empty | Discord user ID mentioned on `level=error` alerts; leave empty to never mention | | `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` | | `LOG_FORMAT` | `json` | `json` or `text` | Example env file: [.env.example](.env.example) ## Local Run ```bash cd alert-bridge cp .env.example .env # edit .env: set API_KEY and DISCORD_WEBHOOK_URL go run ./cmd/bridge ``` ```bash curl -X POST http://localhost:8080/alerts \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"source":"ba-cronjob","message":"Started","level":"info"}' ``` ## Usage From Another LAN Host Deployed at `http://alert-bridge.home.arpa` (plain HTTP, not HTTPS — see "Deployment" below for why). Any host on the home network can reach it directly, no VPN/tunnel/cluster access needed: ```bash curl -X POST http://alert-bridge.home.arpa/alerts \ -H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source":"ba-cronjob","message":"Started","level":"info"}' # ... job runs ... curl -X POST http://alert-bridge.home.arpa/alerts \ -H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source":"ba-cronjob","message":"Finished with 0 errors","level":"info"}' ``` A `level=error` call additionally pings the configured `MENTION_USER_ID` in Discord: ```bash curl -X POST http://alert-bridge.home.arpa/alerts \ -H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source":"ba-cronjob","message":"Finished with 3 errors","level":"error"}' ``` Reusable shape for a cronjob script — export `ALERT_BRIDGE_API_KEY` once at the top (never hardcode the token in the script itself) and call `alert` at each step: ```bash #!/usr/bin/env bash set -euo pipefail ALERT_URL="http://alert-bridge.home.arpa/alerts" SOURCE="ba-cronjob" alert() { local level="$1" message="$2" curl -sf -X POST "$ALERT_URL" \ -H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"source\":\"$SOURCE\",\"message\":\"$message\",\"level\":\"$level\"}" \ > /dev/null } alert info "Started" if ! run_the_actual_job; then alert error "Finished with errors" exit 1 fi alert info "Finished with 0 errors" ``` Since this is plain HTTP, the bearer token travels in cleartext on the LAN — acceptable for a home network, but don't reuse `ALERT_BRIDGE_API_KEY` for anything more sensitive. ## Test And Build ```bash go test ./... go build ./... ``` Build the container image from the workspace root: ```bash docker build -f alert-bridge/Dockerfile -t alert-bridge:dev . ``` ## Package Map ```text cmd/bridge/ # process entrypoint and wiring internal/adapters/primary/http/ # POST /alerts handler: auth, decode, validate internal/adapters/secondary/discordwebhook/ # builds the embed payload, posts to the Discord webhook internal/app/ # thin orchestration between the HTTP handler and the Notifier internal/core/domain/ # Alert{Source, Message, Level} internal/core/ports/driven/ # Notifier interface internal/config/ # environment loading internal/logger/ # slog setup internal/telemetry/ # OpenTelemetry setup ``` ## Deployment Kubernetes manifests (Deployment/Service, plus a plain-HTTP Traefik `IngressRoute` at `alert-bridge.home.arpa` so LAN callers outside the cluster can reach it) live in the separate `homelab` repo, not here. Deployed and confirmed working as of 2026-08-01. The ingress is plain HTTP rather than the internal-CA HTTPS every other `*.home.arpa` service uses — LAN callers (e.g. `nik-gpu`) don't have this cluster's internal CA trusted, and installing it on every caller was judged not worth it for an endpoint that's already bearer-token-authenticated and LAN-only. See `homelab/manifests/home-services/alert-bridge-ingress.yaml`'s header comment for the full reasoning. ## Limitations - Single channel/webhook only — no per-source routing to multiple channels. - No rate limiting of its own; relies on Discord's own webhook rate limits (5 requests/2s per webhook), which is more than enough for cronjob-scale notification volume. - Fire-and-forget only: no message editing, threading, or reaction handling. That would require routing through `discord-bot`'s live bot session instead of a plain webhook — not needed for the current use case.