16 KiB
ntfy-bridge — Architecture
Philosophie
ntfy-bridge est un daemon HTTP léger écrit en V (~2000 lignes) qui compile en un seul binaire natif statique. Il écoute des webhooks, se connecte au socket Docker, exécute des plugins externes, et poll des endpoints HTTP — puis transforme chaque événement en notification Ntfy formatée.
Principes :
- Un binaire, zéro runtime — pas de Node, Python, JVM ou conteneur obligatoire
- Config-driven — tout passe par le fichier YAML, pas de recompilation
- Fail-safe — un échec réseau vers Ntfy ne crashe pas le daemon, retry avec backoff
- Silencieux par défaut — ne spamme pas, utilise les bons niveaux de priorité Ntfy (1-5)
- Extensible — ajouter une source = ajouter un fichier dans
sources/+ une route webhook - Zéro dépendance externe — 100% stdlib V (
net.http,json,yaml,time,os,flag)
Architecture globale
┌─────────────────────────────────────────────────────────────┐
│ ntfy-bridge │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │
│ │ HTTP │ │ Docker │ │ HTTP │ │ Cron/ │ │ Plugin │ │
│ │ Server │ │ Watcher │ │ Poller │ │ Generic │ │ Runner │ │
│ │ :9090 │ │ goroutine│ │ goroutine│ │ Receiver │ │ goroutine│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ └────┬─────┘ │
│ │ │ │ │ │ │
│ └──────────────┼──────────────┼──────────────┼──────────────┘ │
│ ▼ ▼ │
│ ┌──────────────────────────┐ │
│ │ Event Pipeline │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │Parse & │ │Dedup & │ │ │
│ │ │Format │──│RateLim │ │ │
│ │ └────────┘ └────────┘ │ │
│ └────────────┬─────────────┘ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Ntfy Publisher (retry) │ │
│ │ POST /<topic> │ │
│ │ 3 tentatives, backoff │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Flux de données
1. Webhook entrant (Gitea, Uptime Kuma, Cron, Generic)
POST /webhooks/gitea-flowdeck
│
▼
1. O(1) lookup dans webhook_routes (map[string]WebhookRoute)
2. Verify HMAC-SHA256 signature (si configuré, global)
3. Dispatcher vers le transformer approprié (sources/transform_*.v)
4. Parse JSON → Event struct
5. Appliquer priority_map
6. Appliquer template de message (via render_source_template)
7. Pipeline publish: apply_defaults → dedup.allow → ntfy.publish
8. POST vers Ntfy avec retry (3 tentatives, backoff 1s/2s/4s)
2. Docker watcher (socket)
goroutine docker_watch_loop (via start_background_tasks)
│
▼
1. Connexion au socket Docker Unix (/var/run/docker.sock)
2. Stream events via GET /events?filters={"die":true,"oom":true,...}
3. Filtrer par événements configurés (die, oom, health_status)
4. Extraire container_name, image, exit_code via transform_docker_event
5. Appliquer template
6. Pipeline publish → Ntfy
3. HTTP Poller
goroutine http_poll_loop (via start_background_tasks, toutes les N secondes)
│
▼
1. GET <url> avec timeout configuré
2. Si status ≠ expected → Event(priority=configurée, tags=["x"])
3. Si OK et précédemment DOWN → Event(priority=1, tags=["white_check_mark"])
4. Sinon → rien (pas de notification si tout va bien)
5. State machine: poll_state[url] = is_up → notification uniquement au changement
4. Cron receiver
POST /webhooks/cron-disk
│
▼
1. Body = texte brut du script cron
2. Pass-through vers Ntfy (transform_cron → Event)
3. Le script cron contrôle son propre format
5. Plugin runner (v0.6)
goroutine plugin_loop (via start_background_tasks, toutes les 60s)
│
▼
1. Exécute le binaire configuré (os.execute)
2. Lit stdout → parse JSON → Event struct
3. Exit 0 = publier, exit ≠ 0 = skip (pas de notif)
4. Supporte priority, tags, message, click_url, actions
5. Pipeline publish → Ntfy
6. ACL pipeline (v0.6)
POST /webhooks/*
│
▼
1. Extraire IP client (X-Forwarded-For > X-Real-IP)
2. Extraire Bearer token (Authorization header)
3. Si ACL configurée sur le webhook → valider IP et/ou token
4. Si échec → 403 Forbidden
5. Sinon → continuer le pipeline normal
Modèle de données
// config.v
struct ServerConfig {
url string // URL du serveur Ntfy
auth_token string // Token d'auth (optionnel, override via NTFY_TOKEN env)
listen string // Adresse d'écoute HTTP (:9090)
hmac_secret string // Secret partagé HMAC-SHA256 (override via NTFY_HMAC_SECRET env)
}
struct DedupConfig {
enabled bool
ttl_seconds int // TTL du cache de déduplication
rate_limit RateLimitConfig // max_per_minute, max_per_source
}
// sources/event.v
struct Event {
source string // "gitea", "docker", "uptime_kuma", "http_poll", "cron", "generic", "plugin"
name string // nom de la source configurée
topic string // topic Ntfy cible
priority int // 1-5
tags []string
message string // message formaté final
raw string // payload brut (pour debug)
click_url string // URL ouverte au clic sur la notif (v0.5)
actions []Action // boutons d'action Ntfy (v0.5)
}
Les types de sources (GiteaSource, UptimeKumaSource, DockerSource, HttpPollSource/HttpCheck, CronSource, GenericSource, PluginSource) et AclConfig sont définis dans sources/event.v.
Pipeline de transformation
Chaque source a une free function dans sources/ :
// Exemple : sources/gitea.v
pub fn transform_gitea(raw string, source GiteaSource) ?Event { ... }
Pas d'interface — les fonctions sont dispatchées via un match route.kind dans server.v. L'ajout d'une nouvelle source nécessite :
- Un fichier
sources/nouveau.vavec latransform_*function - Un type source dans
sources/event.v - Une entrée dans
SourcesConfig(dansconfig.v) - Une branche dans
build_webhook_routesethandle_webhook
Transformers inclus
| Transformer | Entrée | Sortie |
|---|---|---|
transform_gitea |
JSON webhook Gitea | 🔨 [FlowDeck] bruno pushed: "Fix bug" (a3f2c1d) |
transform_uptime_kuma |
JSON webhook Kuma | 🚨 Gitea is DOWN — 503 — since 14:32 |
transform_docker_event |
Docker event JSON | 🐳 flowdeck exited on docker-prod-1 |
transform_http_down/up |
HTTP response | ❌ og.dracodev.net/health → timeout |
transform_cron |
Raw body | Pass-through, pas de transformation |
transform_generic |
Raw body | Pass-through, pas de transformation |
transform_plugin |
stdout d'un exécutable externe | JSON → Event (exit 0 = publish, ≠ 0 = skip) |
Template engine
render_source_template(template, vars) dans sources/event.v — remplacement simple {variable} → valeur. Supporté par Gitea, Uptime Kuma, Docker, HTTP poll.
template: "🐳 {container_name} → {status}\nImage: {image}\nHost: {host}"
Priorités Ntfy
| Niveau | Usage | Exemple |
|---|---|---|
| 5 (urgent) | Service critique down, container OOM | Uptime Kuma DOWN |
| 4 (high) | PR ouverte, container crash | Gitea pull_request, Docker die |
| 3 (default) | Push, issue, activité normale | Gitea push |
| 2 (low) | Info, succès | Service back UP |
| 1 (min) | Debug, heartbeat | Health check OK |
Déduplication et rate limiting
- Déduplication : hash du message (FNV-1a-like) → cache avec TTL configurable (défaut: 5 min). Clé =
source:name:topic:hash(message). Un message identique dans la fenêtre TTL est ignoré. - Rate limiting par topic :
max_per_minutenotifications max par topic (défaut: 10). Fenêtre glissante de 60s. - Rate limiting par source :
max_per_sourcenotifications max par source configurée (défaut: 30). Fenêtre glissante de 60s. - Nettoyage automatique : les entrées expirées sont nettoyées à chaque appel
allow().
Structure du projet
ntfy-bridge/
├── README.md # Vue d'ensemble, quickstart
├── docs/
│ ├── ARCHITECTURE.md # Ce document
│ ├── ROADMAP.md # Phases de développement
│ ├── CONTRIBUTING.md # Guide de contribution
│ └── WEBHOOK_GITEA.md # Guide config webhook Gitea
├── ntfy-bridge.example.yaml # Exemple de configuration complet
├── main.v # Entrypoint, parsing CLI, header ASCII
├── config.v # Chargement + validation YAML, env overrides
├── server.v # HTTP server, routing webhooks O(1), dashboard, API
├── ntfy.v # Client HTTP Ntfy (POST + retry backoff)
├── dedup.v # Déduplication + rate limiting (topic + source)
├── template.v # Application des defaults (priority, tags)
├── log.v # Logging structuré (human + JSON)
├── webhook.v # O(1) route dispatch (WebhookRoute map)
├── docker_watcher.v # Watcher socket Docker (Unix seulement)
├── filter_outgoing.v # Filtres avancés + outgoing webhooks + state persistence
├── metrics.v # Endpoint Prometheus /metrics
├── service.v # Installation/désinstallation de service (systemd, openrc, nssm)
├── docker_install.v # Génération de stack docker-compose
├── dashboard.html # Interface web : stats, status, historique
├── sources/ # Types Event + transformers
│ ├── event.v # Types Event, Source, render_source_template
│ ├── gitea.v # Transformer Gitea (push, PR, issue, release)
│ ├── uptime_kuma.v # Transformer Uptime Kuma (state_map)
│ ├── docker.v # Transformer Docker events
│ ├── http_poll.v # Transformer HTTP poll (up/down)
│ ├── cron.v # Transformer Cron (pass-through)
│ ├── generic.v # Transformer Generic (pass-through)
│ └── plugin.v # Transformer Plugin (exec stdout JSON)
├── *_test.v # Tests unitaires (config, gitea, uptime_kuma, dedup, filter_outgoing, metrics, acl)
├── scripts/
│ ├── example-cron-disk.sh # Exemple script cron → Ntfy
│ ├── example-plugin-disk.sh # Exemple plugin disk check
│ ├── install.sh # Script d'installation Linux one-liner
│ └── install.ps1 # Script d'installation Windows PowerShell
└── v.mod # Dépendances V (aucune dépendance externe)
Dépendances
// v.mod
Module {
name: 'ntfy-bridge'
version: '0.7.0'
dependencies: ['https://github.com/vlang/yaml'] // parsing YAML
}
Le projet utilise la stdlib V + le module yaml :
net.http— serveur HTTP + client Ntfyx.json2— parsing JSONyaml— parsing YAML (module externe vlang/yaml)crypto.hmac,crypto.sha256,encoding.hex— validation HMACtime,os,flag— utilitairesnet.unix— socket Docker (Linux/macOS seulement, via$if)
Endpoints HTTP
| Méthode | Path | Description |
|---|---|---|
| GET | / ou /dashboard |
Dashboard HTML |
| GET | /health |
Health check → {"status":"ok","uptime_seconds":N,"total_notifications":N} |
| GET | /stats |
Stats par source → {"total":N,"errors":N,"by_source":{...}} |
| GET | /api/config |
Config résumée (safe, sans secrets) |
| GET | /api/status |
État des health checks HTTP poll |
| GET | /api/history |
100 derniers événements |
| GET | /api/silence |
État du silence (actif/restant) |
| POST | /api/silence?duration=30m |
Activer le silence |
| DELETE | /api/silence |
Désactiver le silence |
| GET | /metrics |
Métriques Prometheus / OpenMetrics |
| POST | /webhooks/* |
Webhooks (Gitea, Uptime Kuma, Cron, Generic) |
Déploiement
Option A : Binaire nu + systemd
# /etc/systemd/system/ntfy-bridge.service
[Unit]
Description=ntfy-bridge notification hub
After=network.target
[Service]
ExecStart=/usr/local/bin/ntfy-bridge --quiet --config /etc/ntfy-bridge.yaml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Option B : Conteneur Docker
# Quick start avec docker compose
cp ntfy-bridge.example.yaml ntfy-bridge.yaml
vim ntfy-bridge.yaml
docker compose up -d
# Dockerfile (build hôte + runtime Alpine minimal)
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata curl
COPY ntfy-bridge /usr/local/bin/ntfy-bridge
RUN chmod +x /usr/local/bin/ntfy-bridge
WORKDIR /etc/ntfy-bridge
EXPOSE 9090
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:9090/health || exit 1
ENTRYPOINT ["/usr/local/bin/ntfy-bridge"]
CMD ["--quiet", "--config", "/etc/ntfy-bridge/ntfy-bridge.yaml"]
Option C : Compilation croisée
v -prod -os windows . -o ntfy-bridge.exe
v -prod -os linux . -o ntfy-bridge
Sécurité
- ACLs par webhook (v0.6) : restriction par IP (CIDR ou exacte) et/ou Bearer token. Validé avant HMAC.
- Signature webhooks : vérification HMAC-SHA256 pour tous les webhooks (header
X-Hub-Signature-256: sha256=...). Activé dès queserver.hmac_secretest défini. - Auth token : token Bearer optionnel pour le serveur Ntfy (si auth activée sur le serveur Ntfy)
- Pas d'exposition externe : le serveur HTTP écoute sur localhost par défaut (
127.0.0.1:9090) - Rate limiting interne : par topic ET par source (configurable dans
dedup.rate_limit) - Input validation : tout JSON entrant est validé avant processing, chemins webhook dupliqués détectés au démarrage
- No secrets in config :
NTFY_TOKENetNTFY_HMAC_SECRETpeuvent être passés via variables d'environnement
Métriques et observabilité
- Health endpoint :
GET /health→ 200 OK + uptime + compteur de notifs - Stats endpoint :
GET /stats→ compteurs par source + erreurs - API status :
GET /api/status→ état des health checks HTTP poll - API history :
GET /api/history→ 100 derniers événements avec timestamps - Dashboard : interface HTML avec stats temps réel, statut des services, historique récent
- Logging : stdout en mode human-readable (console) ou JSON structuré (
--quiet, pour systemd)