No description
  • Go 73.8%
  • Kotlin 25.7%
  • Dockerfile 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-26 20:28:46 +02:00
android docs(android): self-hosted UnifiedPush setup guide 2026-09-26 20:13:17 +02:00
assets docs: project branding — custom SVG logo and polished README with badges 2026-09-24 14:29:11 +02:00
cmd/server feat: device registry, async push dispatcher (FCM/UnifiedPush), Synology + Unraid adapters 2026-09-25 12:07:16 +02:00
internal feat(api): WebSocket live stream alongside SSE 2026-09-26 20:28:18 +02:00
.gitignore feat(android): native Compose client — UnifiedPush receiver, Room history, dark UI 2026-09-25 12:24:28 +02:00
docker-compose.yml feat: NotiGate MVP backend — ingestion, SSE streaming, hardened proxy path 2026-09-24 14:14:46 +02:00
Dockerfile feat: NotiGate MVP backend — ingestion, SSE streaming, hardened proxy path 2026-09-24 14:14:46 +02:00
go.mod feat(api): WebSocket live stream alongside SSE 2026-09-26 20:28:18 +02:00
go.sum feat(api): WebSocket live stream alongside SSE 2026-09-26 20:28:18 +02:00
README.md docs: bump release badge to v0.4.0 — WebSocket live stream milestone 2026-09-26 20:28:46 +02:00

NotiGate logo

NotiGate

One self-hosted gateway for every alert.
push-first · battery-friendly · delay-free

Release v0.4.0 Go 1.26+ CGO-free single binary SQLite WAL mode SSE live stream Distroless Docker image Tests passing


NotiGate is the universal ingestion hub for your homelab's alerts: Unraid, Synology, Proxmox, Uptime Kuma, Grafana, Watchtower and plain shell scripts all feed into one normalized event schema — and NotiGate delivers them delay-free to dashboards and, soon, mobile devices that only wake up when an event actually arrives.

 Unraid ──────┐                              ┌──────►  Dashboards · live (SSE/WebSocket)
 Synology ────┤                              │
 Proxmox ─────┼──►  NotiGate ────────────────┼──────►  History API · JSON
 Uptime Kuma ─┤    (normalize + route)       │
 Scripts ─────┘                              └──────►  Android · push (soon)

✨ Why NotiGate?

  • 🔔 One schema for everything — every adapter normalizes into a single internal event model (source, title, message, priority, tags, click_url, timestamp).
  • ⚡ Delay-free ingestion — non-blocking broadcast hub; incoming webhooks are acked in milliseconds.
  • 🔋 Battery-friendly by design — push/stream transport with wake-on-event, no polling loops draining phones.
  • 🛡️ Hardened by default — trusted-proxy Real-IP resolution (spoof-proof), token-bucket rate limiting, constant-time token auth.
  • 🔌 Drop-in compatibility — Discord-webhook mode means Uptime Kuma, Watchtower, Grafana and friends work without a special adapter.
  • 📦 Operationally boring — one CGO-free static binary, SQLite in WAL mode, distroless container image.

🚀 Quick start

go build -o notigate ./cmd/server
API_KEYS="$(openssl rand -hex 32)" ./notigate

Without API_KEYS the server generates an ephemeral key and logs it at startup (dev convenience — set API_KEYS in production).

curl -X POST http://localhost:8080/api/v1/send \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source":"unraid","title":"Array Alert","message":"Disk 2 has SMART errors","priority":4,"tags":["hardware","storage"],"click_url":"https://unraid.lan"}'

🔌 Endpoints

Method Path Auth Purpose
GET /healthz – Liveness for Docker / proxy upstream checks (+ version)
POST /api/v1/send yes Canonical JSON ingestion (202 + normalized event)
POST /api/v1/ingest/discord yes Discord-webhook compatible mode (204), e.g. Uptime Kuma
POST /api/v1/ingest/synology yes Synology DSM placeholders (%SUBJECT%, %TEXT%)
POST /api/v1/ingest/unraid yes Unraid notify fields (JSON or form)
GET /api/v1/stream yes Server-Sent Events live stream
GET /api/v1/stream/ws yes WebSocket live stream (same JSON objects as text frames)
GET /api/v1/notifications yes History, newest first (?limit=100)
POST /api/v1/devices yes Register a push device (FCM / UnifiedPush)
GET /api/v1/devices yes List devices (credentials never echoed)
DELETE /api/v1/devices/{id} yes Remove a device

Auth: Authorization: Bearer <key> or ?token=<key> (for webhooks and EventSource, which cannot set headers). Comparison is constant-time.

📦 Event schema

Every ingestion channel normalizes onto this single schema:

{
  "id": "uuid-v4",
  "source": "unraid",
  "title": "Array Alert",
  "message": "Disk 2 has SMART errors",
  "priority": 4,
  "tags": ["hardware", "storage"],
  "click_url": "https://unraid.lan",
  "timestamp": 1727180000
}

Priorities follow the ntfy convention: 0 = min … 5 = max, 3 = default. Discord embed colors are mapped automatically (green → 1, yellow → 4, red → 5).

📱 Mobile push (FCM / UnifiedPush)

Notifications are pushed asynchronously by a worker pool — ingestion is acked in milliseconds and delivery happens in the background:

  • UnifiedPush (self-hosted, no Google): register the distributor endpoint of any UnifiedPush app (ntfy, NextPush, …):

    curl -X POST http://localhost:8080/api/v1/devices \
      -H "Authorization: Bearer $API_KEY" \
      -d '{"name":"Phone","provider":"unifiedpush","endpoint":"https://ntfy.lan/topic-xyz"}'
    

    The normalized notification arrives as a plain JSON POST on that endpoint. Endpoints answering 404/410 are removed automatically.

  • FCM (for the Android app): set FCM_CREDENTIALS_FILE to a Google service-account JSON key with the Firebase Cloud Messaging API enabled. Access tokens are minted by signing RS256 JWTs — no Google SDK involved. Register devices with {"provider":"fcm","token":"<registration token>"}; unregistered tokens (404/410) are pruned automatically.

Device credentials are stored in SQLite but never returned by the API.

🧩 Adapter notes

  • Synology DSM — point the DSM notification webhook at /api/v1/ingest/synology?token=…&subject=%SUBJECT%&text=%TEXT%.
  • Unraid — POST the fields of Unraid's notify script (event, subject, description, content, importance) as JSON or form; importance maps alert→5, warning→4, normal→3.

📡 SSE & WebSocket behind reverse proxies

The stream sets X-Accel-Buffering: no, Cache-Control: no-cache and sends a heartbeat comment every 30 s, so nginx (NPM), Caddy and Traefik neither buffer nor drop idle connections. For EventSource clients, pass the token as a query parameter — note that it can appear in proxy access logs, so keep ALLOWED_ORIGINS tight for browser clients.

The WebSocket stream (/api/v1/stream/ws) carries the same notification JSON objects as text frames, plus protocol-level pings every ~54 s. nginx needs the standard upgrade headers for it:

location /api/v1/stream/ws {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass http://notigate;
}

WS upgrades are not CORS-preflighted, so the endpoint enforces the ALLOWED_ORIGINS policy itself: requests without Origin (server-side clients) pass, browser origins must be listed.

⚙️ Configuration

Variable Default Meaning
LISTEN_ADDR :8080 Listen address
DB_PATH data/notigate.db SQLite file (WAL mode, CGO-free driver)
API_KEYS (ephemeral, logged) Comma-separated valid API keys
TRUSTED_PROXIES 127.0.0.0/8,::1/128 CIDRs allowed to set Forwarded/X-Forwarded-For/X-Real-IP
ALLOWED_ORIGINS * CORS origins for dashboards
RATE_LIMIT_RPS 20 Token-bucket refill per client IP
RATE_LIMIT_BURST 40 Token-bucket capacity
PUSH_WORKERS 8 Async push delivery workers
PUSH_QUEUE 1024 Push job queue size (full queue drops + logs)
FCM_CREDENTIALS_FILE (disabled) Service-account JSON key to enable FCM delivery

Real-IP resolution only honors proxy headers when the direct TCP peer is in TRUSTED_PROXIES; everything else is treated as the client itself, which prevents IP spoofing in the rate limiter and logs. Precedence: RFC 7239 Forwarded → X-Real-IP → X-Forwarded-For (right-to-left walk).

🏷️ Versioning

Build metadata is injected at link time and exposed through the -version flag, the startup log and /healthz:

go build -trimpath -ldflags "-s -w \
  -X notigate/internal/version.Version=$(git describe --tags --always) \
  -X notigate/internal/version.Commit=$(git rev-parse --short HEAD) \
  -X notigate/internal/version.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -o notigate ./cmd/server
./notigate -version

Docker takes the same values as build args:

docker build --build-arg VERSION=$(git describe --tags --always) \
  --build-arg COMMIT=$(git rev-parse --short HEAD) \
  --build-arg DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) -t notigate .

Releases are tagged vX.Y.Z (annotated tags).

🐳 Docker

docker compose up -d --build

Distroless static image, single CGO-free binary, state under /data.

🗺️ Roadmap

  • JSON ingestion (/api/v1/send) + Discord webhook compatibility
  • SSE live streaming with proxy-safe heartbeats
  • Trusted-proxy Real-IP, token-bucket rate limiting, constant-time auth
  • Native Synology DSM + Unraid adapters
  • Device registry + async push dispatcher (FCM HTTP v1 / UnifiedPush)
  • Native Android app (Compose) consuming UnifiedPush
  • WebSocket transport alongside SSE
  • Token management UI backed by SQLite

NotiGate
Made with ⚡ and Go — crafted for the self-hosted homelab.