Files
ntfy-bridge/ARCHITECTURE.md
T
bruno 3f48912ad7 docs: initial architecture, roadmap, readme for ntfy-bridge
- README.md: project overview, quickstart, sources summary
- ARCHITECTURE.md: complete architecture, data flow, project structure
- ROADMAP.md: 5-phase development plan (v0.1.0 → v0.5.0+)
- CONTRIBUTING.md: dev setup, conventions, how to add a source
- ntfy-bridge.example.yaml: fully documented configuration template
- scripts/example-cron-disk.sh: example cron script → ntfy-bridge
2026-08-01 10:56:32 -04:00

10 KiB

ntfy-bridge — Architecture

Philosophie

ntfy-bridge est un daemon HTTP léger écrit en V (~500 lignes) qui compile en un seul binaire natif statique. Il écoute des webhooks, se connecte au socket Docker, 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
  • Silencieux par défaut — ne spamme pas, utilise les bons niveaux de priorité Ntfy (1-5)
  • Extensible — ajouter une source = implémenter un module dans sources/

Architecture globale

┌─────────────────────────────────────────────────────────────┐
│                        ntfy-bridge                          │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────────┐ │
│  │  HTTP    │  │  Docker  │  │  HTTP    │  │  Cron      │ │
│  │  Server  │  │  Watcher │  │  Poller  │  │  Receiver  │ │
│  │ :9090    │  │  goroutine│ │  goroutine│ │  /webhooks │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └─────┬──────┘ │
│       │              │              │              │        │
│       └──────────────┼──────────────┼──────────────┘        │
│                      ▼              ▼                       │
│              ┌──────────────────────────┐                   │
│              │     Event Pipeline       │                   │
│              │  ┌────────┐  ┌────────┐  │                   │
│              │  │Parse & │  │Dedup & │  │                   │
│              │  │Format  │──│RateLim │  │                   │
│              │  └────────┘  └────────┘  │                   │
│              └────────────┬─────────────┘                   │
│                           ▼                                 │
│              ┌──────────────────────────┐                   │
│              │     Ntfy Publisher       │                   │
│              │  POST /<topic>           │                   │
│              └──────────────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

Flux de données

1. Webhook entrant (Gitea, Uptime Kuma, generic)

POST /webhooks/gitea-flowdeck
   │
   ▼
1. Verify signature (optionnel)
2. Parse JSON → Event struct
3. Route vers le transformer approprié
4. Appliquer priority_map
5. Appliquer template de message
6. POST vers Ntfy

2. Docker watcher (socket)

goroutine DockerWatcher
   │
   ▼
1. Connexion au socket Docker (/var/run/docker.sock)
2. Stream events via GET /events?filters=...
3. Filtrer par événements configurés (die, oom, health_status)
4. Extraire container_name, image, exit_code
5. Appliquer template
6. POST vers Ntfy

3. HTTP Poller

goroutine HttpPoller (toutes les N secondes)
   │
   ▼
1. GET <url> avec timeout configuré
2. Si status ≠ expected → Event(priority=5)
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)

4. Cron receiver

POST /webhooks/cron-disk
   │
   ▼
1. Body = texte brut du script cron
2. Pass-through vers Ntfy
3. Le script cron contrôle son propre format

Modèle de données

struct Config {
    server ServerConfig
    defaults DefaultConfig
    sources SourcesConfig
}

struct ServerConfig {
    url         string  // URL du serveur Ntfy
    auth_token  string  // Token d'auth (optionnel)
    listen      string  // Adresse d'écoute HTTP (:9090)
}

struct DefaultConfig {
    priority  int      // 1-5
    tags      []string // ["loudspeaker"]
}

struct Source {
    name         string
    webhook_path string
    topic        string
    template     string  // Go-like template
    priority_map map[string]int
    tags         []string
}

struct Event {
    source    string   // "gitea", "docker", "uptime_kuma", "http_poll", "cron"
    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)
}

Pipeline de transformation

Chaque source implémente l'interface :

interface Transformer {
    transform(raw string, source &Source) ?Event
}

Transformers inclus

Transformer Entrée Sortie
gitea_transformer JSON webhook Gitea 🔨 [FlowDeck] bruno pushed to main: "Fix bug" (a3f2c1d)
uptime_kuma_transformer JSON webhook Kuma 🚨 Gitea is DOWN — 503 — since 14:32
docker_transformer Docker event JSON 🐳 flowdeck exited OOMKilled on docker-prod-1
http_poll_transformer HTTP response ❌ obsigate.dracodev.net/health → timeout 30s
generic_transformer Raw body Pass-through, pas de transformation

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 (si configuré)

Déduplication et rate limiting

  • Déduplication : même source + même type d'événement + même cible → cooldown 5 minutes
  • Rate limiting : max 10 notifs/minute/topic (configurable)
  • Uptime Kuma spécifique : les "down" répétés dans un intervalle de 2 min sont dédupliqués

Structure du projet

ntfy-bridge/
├── README.md                   # Vue d'ensemble, quickstart
├── ARCHITECTURE.md             # Ce document
├── ROADMAP.md                  # Phases de développement
├── CONTRIBUTING.md             # Guide de contribution
├── ntfy-bridge.example.yaml    # Exemple de configuration complet
├── src/
│   ├── main.v                  # Entrypoint, parsing CLI
│   ├── config.v                # Chargement + validation YAML
│   ├── server.v                # HTTP server + routing webhooks
│   ├── event.v                 # Types Event, pipeline
│   ├── ntfy.v                  # Client HTTP Ntfy (POST)
│   ├── dedup.v                 # Déduplication + rate limiting
│   ├── template.v              # Mini moteur de template
│   ├── sources/
│   │   ├── gitea.v             # Transformer Gitea
│   │   ├── uptime_kuma.v       # Transformer Uptime Kuma
│   │   ├── docker.v            # Watcher Docker socket
│   │   ├── http_poll.v         # Poller HTTP périodique
│   │   └── generic.v           # Webhook passe-partout
│   └── log.v                   # Logging structuré
├── tests/
│   ├── config_test.v           # Tests validation config
│   ├── gitea_test.v            # Tests transformer Gitea
│   ├── uptime_kuma_test.v      # Tests transformer Kuma
│   └── dedup_test.v            # Tests déduplication
├── scripts/
│   └── example-cron-disk.sh    # Exemple script cron → Ntfy
└── v.mod                       # Dépendances V

Dépendances V (v.mod)

Module {
    name: 'ntfy-bridge'
    description: 'Homelab notification hub — aggregates Gitea, Docker, Uptime Kuma → Ntfy'
    version: '0.1.0'
    deps: [
        'ui-driver.cua'
    ]
}

Dépendances externes minimales :

  • vsl — parsing YAML (sinon implémenter un parseur minimal)
  • x.net — client HTTP (ou lib standard V net)
  • json — stdlib V pour le parsing JSON

En réalité, V a tout dans sa stdlib : net.http, json, time, os, flag. Le projet vise zéro dépendance externe.

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 --config /etc/ntfy-bridge.yaml
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Option B : Conteneur Docker

FROM alpine:latest
COPY ntfy-bridge /usr/local/bin/
COPY ntfy-bridge.yaml /etc/
EXPOSE 9090
CMD ["/usr/local/bin/ntfy-bridge", "--config", "/etc/ntfy-bridge.yaml"]

Option C : Compilation croisée

v -os windows . -o ntfy-bridge.exe   # Pour Windows
v -os linux . -o ntfy-bridge          # Pour Linux
v -os macos . -o ntfy-bridge-mac      # Pour macOS

Sécurité

  • Signature webhooks : vérification HMAC-SHA256 optionnelle pour Gitea
  • Auth token : token optionnel pour le serveur Ntfy (si auth activée)
  • Pas d'exposition externe : le serveur HTTP écoute sur localhost par défaut (:9090)
  • Rate limiting interne : max 10 notifs/minute/source pour éviter le spam
  • Input validation : tout JSON entrant est validé avant processing
  • No secrets in config : le token Ntfy peut être passé via variable d'environnement NTFY_TOKEN

Métriques et observabilité

  • Health endpoint : GET /health → 200 OK + uptime + compteur de notifs
  • Stats endpoint : GET /stats → compteurs par source, erreurs, latence Ntfy
  • Logging : stdout en JSON structuré (timestamp, level, source, event, error)