Haiven Notification Hub — User Guide

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.

Table of Contents

  1. Quick Start
  2. Sending Notifications
  3. Channel Reference
  4. Routing Configuration
  5. Priority Levels
  6. Integration Guide
  7. Configuration Reference
  8. Troubleshooting
  9. Monitoring

Quick Start

Access Points

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

Send Your First Notification

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"
}

Sending Notifications

POST /notify

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

Response Fields

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)

Channel Resolution Order

When deciding which channels receive a notification, the hub applies these rules in order:

  1. If channels field is non-empty, use it directly (skips routing table entirely)
  2. Look up source_agent in the routing table
  3. Fall back to the default routing entry
  4. Hard fallback: ["email"]

Channel Reference

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).

ntfy

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"]}'

ha_tts

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"]}'

Routing Configuration

Routes are stored in a YAML file mounted at /etc/haiven/notification-routes.yaml
(host path: /mnt/storage/notification-hub/routes/notification-routes.yaml).

Format

routes:
  briefing: ["email", "ntfy"]
  research: ["email"]
  eod: ["email", "ntfy"]
  default: ["email"]

Rules

Applying Changes

Routes 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

Example: Route a new agent

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 Levels

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


Integration Guide

From a Python agent

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()

From a shell script

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

From the briefing agent

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}'

Explicit channel override

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"]
  }'

Configuration Reference

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

Enabling optional channels

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

Troubleshooting

No email received

  1. Verify HUB_SMTP_TO is set: docker exec notification-hub env | grep HUB_SMTP
  2. Check Mailpit UI at https://mail.haiven.site — messages appear even if delivery to
    external addresses is not configured
  3. Check container logs: docker logs notification-hub --tail 50
  4. Verify smtp-relay container is running: docker ps --filter name=smtp-relay

status: "failed" in response

All resolved channels reported errors. Common causes:

Check logs for the specific error:

docker logs notification-hub --tail 100 | grep -E "failed|error"

status: "partial" in response

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 not updating after file change

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

Container fails to start

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

Monitoring

Health check

curl -sf http://localhost:8040/health
# Returns: {"status": "ok"}

Prometheus metrics

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.

Structured logs

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

Support