Multi-channel notification dispatcher for Haiven agent services. Accepts structured
notification payloads and routes them to email (SMTP via Mailpit relay), ntfy.sh push
notifications, or Home Assistant TTS announcements based on a YAML routing table.
| Interface | URL | Purpose |
|---|---|---|
| API | https://hub.haiven.site |
Notification dispatch endpoint |
| Internal | http://notification-hub:8000 |
Docker-internal access for agents |
| Health | https://hub.haiven.site/health |
Liveness probe |
| Metrics | https://hub.haiven.site/metrics |
Prometheus metrics |
curl -X POST https://hub.haiven.site/notify \
-H "Content-Type: application/json" \
-d '{
"source_agent": "test",
"title": "Hello from Haiven",
"body": "Notification hub is working correctly.",
"priority": "normal"
}'
Expected response:
{
"notification_id": "550e8400-e29b-41d4-a716-446655440000",
"channels_dispatched": ["email"],
"status": "ok"
}
The core endpoint accepts a structured payload and delivers it to the resolved channel set.
Full request body:
{
"source_agent": "briefing",
"priority": "high",
"title": "Daily Briefing Ready",
"body": "## Morning Briefing\n\nYou have **5 open tasks**...",
"channels": [],
"metadata": {
"scope": "daily",
"task_count": 5
}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source_agent |
string | yes | — | Agent name used to look up routes (e.g. briefing, research, eod) |
title |
string | yes | — | Notification title (subject line for email) |
body |
string | yes | — | Notification body (markdown supported) |
priority |
string | no | normal |
low, normal, high, urgent |
channels |
array | no | [] |
Explicit channel override — bypasses routing table when non-empty |
metadata |
object | no | {} |
Arbitrary key-value pairs forwarded to channel implementations |
| Field | Type | Description |
|---|---|---|
notification_id |
string (UUID) | Unique ID for this notification |
channels_dispatched |
array | Channel names that received the notification successfully |
status |
string | ok (all channels succeeded), partial (some failed), failed (none succeeded) |
When deciding which channels receive a notification, the hub applies these rules in order:
channels field is non-empty, use it directly (skips routing table entirely)source_agent in the routing tabledefault routing entry["email"]Delivers via SMTP through the smtp-relay (Mailpit) container. Sends an HTML email with
automatic markdown-to-HTML conversion for the body.
Features:
- Automatic markdown rendering (bold, italic, headings, code blocks, lists)
- Plain-text fallback included for email clients that do not render HTML
- Timeout: 15 seconds per send attempt
Required configuration: HUB_SMTP_TO must be set.
Test this channel:
curl -X POST http://localhost:8040/notify \
-H "Content-Type: application/json" \
-d '{"source_agent": "test", "title": "Email Test", "body": "Test body", "channels": ["email"]}'
View delivered emails at https://mail.haiven.site (Mailpit UI).
Delivers a push notification to a self-hosted or public ntfy.sh instance. Uses ntfy's
header-based publish API.
Features:
- Priority mapped from Haiven levels to ntfy levels
- Source agent tag added automatically from metadata when present
- Timeout: 10 seconds per request
Required configuration: HUB_NTFY_URL must be set.
Priority mapping:
| Haiven Priority | ntfy Priority |
|---|---|
low |
low |
normal |
default |
high |
high |
urgent |
urgent |
Test this channel:
curl -X POST http://localhost:8040/notify \
-H "Content-Type: application/json" \
-d '{"source_agent": "test", "title": "Push Test", "body": "Push notification", "channels": ["ntfy"]}'
Posts a JSON payload to a Home Assistant webhook URL, triggering a TTS announcement on
configured media players.
Features:
- Strips markdown symbols before speaking
- Truncates to 300 characters to keep announcements brief
- Format: "<title>: <body excerpt>"
- Timeout: 10 seconds per request
Required configuration: HUB_HA_WEBHOOK_URL must be set.
Test this channel:
curl -X POST http://localhost:8040/notify \
-H "Content-Type: application/json" \
-d '{"source_agent": "test", "title": "TTS Test", "body": "Voice announcement test", "channels": ["ha_tts"]}'
Routes are stored in a YAML file mounted at /etc/haiven/notification-routes.yaml
(host path: /mnt/storage/notification-hub/routes/notification-routes.yaml).
routes:
briefing: ["email", "ntfy"]
research: ["email"]
eod: ["email", "ntfy"]
default: ["email"]
source_agent name.email, ntfy, ha_tts).default key is the fallback for agents not listed.default: ["email"] withoutRoutes are loaded once at startup. To apply a new routes file:
# Edit the file
nano /mnt/storage/notification-hub/routes/notification-routes.yaml
# Restart the container to reload
docker compose -f /mnt/apps/docker/ai/notification-hub/docker-compose.yml restart notification-hub
Add a new agent route without touching any code:
routes:
briefing: ["email", "ntfy"]
research: ["email"]
eod: ["email", "ntfy"]
my-new-agent: ["ntfy", "ha_tts"] # adds announcement + push for a new agent
default: ["email"]
| Priority | Use Case |
|---|---|
low |
Background status, completed batch jobs |
normal |
Standard briefings and routine updates |
high |
Action required, deadlines approaching |
urgent |
System alerts, failures requiring immediate attention |
Priority affects channel behavior differently:
- email: Priority appears in log output; email clients do not differentiate urgency
- ntfy: Priority maps directly to ntfy notification urgency (affects sound, vibration, persistence)
- ha_tts: Priority is not applied; all TTS messages are delivered the same way
import httpx
async def send_notification(title: str, body: str, priority: str = "normal"):
async with httpx.AsyncClient() as client:
response = await client.post(
"http://notification-hub:8000/notify",
json={
"source_agent": "my-agent",
"priority": priority,
"title": title,
"body": body,
},
timeout=15.0,
)
response.raise_for_status()
return response.json()
curl -sf -X POST http://notification-hub:8000/notify \
-H "Content-Type: application/json" \
-d "{\"source_agent\": \"scheduler\", \"title\": \"Job complete\", \"body\": \"Backup finished at $(date -Iseconds)\"}" \
| jq .status
The briefing agent (haiven-agent-briefing) calls the hub automatically when
notify=true is passed in the briefing request. No manual integration needed.
curl -X POST http://localhost:8035/briefing \
-H "Content-Type: application/json" \
-d '{"scope": "daily", "template": "briefing-daily", "notify": true}'
To bypass routing and send to specific channels regardless of routing config:
curl -X POST http://localhost:8040/notify \
-H "Content-Type: application/json" \
-d '{
"source_agent": "maintenance",
"title": "Alert",
"body": "Disk usage exceeded 90%",
"priority": "urgent",
"channels": ["email", "ntfy", "ha_tts"]
}'
Configure via environment variables in the compose file or .env:
| Variable | Default | Description |
|---|---|---|
HUB_SMTP_HOST |
smtp-relay |
SMTP relay hostname (Mailpit container name) |
HUB_SMTP_PORT |
1025 |
SMTP port (Mailpit listens on 1025) |
HUB_SMTP_FROM |
haiven@haiven.site |
From address on outgoing emails |
HUB_SMTP_TO |
"" |
Recipient email address (required for email delivery) |
HUB_NTFY_URL |
"" |
ntfy.sh endpoint URL (e.g. http://ntfy.example.com) |
HUB_NTFY_TOPIC |
haiven |
ntfy topic name |
HUB_HA_WEBHOOK_URL |
"" |
Home Assistant webhook URL for TTS announcements |
HUB_ROUTES_FILE |
/etc/haiven/notification-routes.yaml |
Path to routes config inside container |
Channels with empty config values are silently skipped. To enable a channel, set the
required variable in /mnt/apps/docker/ai/notification-hub/.env:
# Email — always on (uses smtp-relay)
HUB_SMTP_TO=elijah@elijahryoung.com
# ntfy push
HUB_NTFY_URL=http://ntfy.haiven.site
HUB_NTFY_TOPIC=haiven
# Home Assistant TTS
HUB_HA_WEBHOOK_URL=http://homeassistant:8123/api/webhook/haiven-notify
HUB_SMTP_TO is set: docker exec notification-hub env | grep HUB_SMTPhttps://mail.haiven.site — messages appear even if delivery todocker logs notification-hub --tail 50docker ps --filter name=smtp-relayAll resolved channels reported errors. Common causes:
HUB_SMTP_TO not set, or smtp-relay container not reachableHUB_NTFY_URL not set or ntfy server unreachableHUB_HA_WEBHOOK_URL not set or Home Assistant webhook not configuredCheck logs for the specific error:
docker logs notification-hub --tail 100 | grep -E "failed|error"
At least one channel succeeded but not all. The channels_dispatched field shows which
ones worked. Check logs for the failing channel:
docker logs notification-hub --tail 100 | grep "error\|skipped"
Routes are loaded once at startup. Restart the container after editing routes:
docker compose -f /mnt/apps/docker/ai/notification-hub/docker-compose.yml restart notification-hub
docker logs notification-hub --tail 50
# Check that /mnt/storage/notification-hub/routes/ exists on the host
ls /mnt/storage/notification-hub/routes/
Create the directory if missing:
mkdir -p /mnt/storage/notification-hub/routes /mnt/storage/notification-hub/logs
curl -sf http://localhost:8040/health
# Returns: {"status": "ok"}
curl http://localhost:8040/metrics
The /metrics endpoint exposes:
# HELP notification_hub_up Service is running
# TYPE notification_hub_up gauge
notification_hub_up 1
The Prometheus scrape label prometheus.scrape=true is already set in the compose file.
The hub is auto-discovered by Prometheus via Docker label-based service discovery.
The service uses structlog for JSON-structured output. Key log events:
| Event | Meaning |
|---|---|
notification_hub.startup |
Service started, shows channel configuration |
notify.received |
Notification accepted from caller |
router.dispatching |
Channels resolved, dispatch starting |
email.sent |
Email delivered to SMTP relay |
ntfy.sent |
ntfy notification posted |
ha_tts.sent |
Home Assistant webhook called |
*.failed |
Channel delivery failure with error detail |
*.skipped |
Channel skipped due to missing configuration |
View live logs:
docker logs -f notification-hub
https://docs.haiven.site (search for notification-hub)https://status.haiven.sitehttps://mail.haiven.site/mnt/apps/src/haiven-notification-hub//mnt/apps/docker/ai/notification-hub/docker-compose.yml