Setup classique : un capteur ou un device IoT déclenche une alerte (intrusion, géofence, capteur out-of-bound, service down…). Première version naïve : on envoie un SMS / push à chaque évènement → spam de notifs si la condition perdure (30 min de problème = 30 notifs).
Deuxième version : un cooldown entre 2 notifs (ex. 5 min). Mieux, mais quand tu as VU la première notif, tu reçois quand même la suivante 5 min plus tard. Friction inutile.
La bonne version : machine d’état « épisode » avec acquittement explicite.
Le modèle
Un épisode = une période continue où la condition d’alerte est vraie. Il a :
started_atended_at(NULL tant que la condition est vraie)ack_token(random, secret, unique à cet épisode)ack_at,ack_method('url'|'button'| NULL)last_alert_at,alert_count
Au plus un seul épisode ouvert par device (unique index partiel WHERE ended_at IS NULL).
Le flow
- Condition d’alerte devient vraie → ouvre un épisode (INSERT), envoie 1ère notif
- Tant que l’épisode n’est pas acquitté ET que la condition reste vraie → re-notif tous les N secondes
- Ack via URL : la notif contient
https://app/ack/<token>→ click →UPDATE ... SET ack_at=NOW(), ack_method='url' - Ack via bouton dans l’UI : POST endpoint →
UPDATE ... ack_method='button' - Une fois ack, plus de re-notif pour cet épisode
- Condition redevient fausse → ferme l’épisode (
ended_at=NOW()) - Si la condition redevient vraie → nouvel épisode avec nouveau token → re-notification
Plus un bouton « Réarmer » dans l’UI : UPDATE ... SET ack_at=NULL, ack_method=NULL — pour les ack par erreur.
Pourquoi un token random PAR ÉPISODE
- Le token n’est valide que pour CET épisode → impossible de pré-acquitter un futur épisode en gardant l’URL précédente
- Pas d’auth nécessaire sur l’endpoint
/ack/:token→ l’utilisateur peut cliquer depuis SMS sans login (le token EST l’auth) - 12 octets random b64url = 16 chars URL-safe → URL courte (compatible SMS)
Risque pré-fetch des URL dans SMS
Certains clients (iMessage, WhatsApp, mobile preview) pré-fetchent les URL affichées en preview → risque d’ack accidentel.
3 options selon la sensibilité :
- GET ack immédiat (simple, OK pour usage perso single-user où le SMS ne fuit nulle part)
- GET = page de confirmation, POST = ack réel (1 click supplémentaire, sécurise contre pré-fetch)
- Token signé par HMAC + endpoint vérifie la signature (parano)
Pour un projet perso avec SMS vers son propre numéro, GET immédiat acceptable. Pour multi-utilisateurs ou notif sensible (admin de prod), la page de confirmation s’impose.
Schéma SQL minimal
CREATE TABLE outings (
id BIGSERIAL PRIMARY KEY,
device_id TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ended_at TIMESTAMPTZ,
ack_token TEXT NOT NULL UNIQUE,
ack_at TIMESTAMPTZ,
ack_method TEXT,
last_alert_at TIMESTAMPTZ,
alert_count INT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX outings_one_open
ON outings (device_id) WHERE ended_at IS NULL;
L’unique index partiel garantit qu’on ne peut jamais avoir 2 épisodes ouverts pour le même device — si on tente, l’INSERT échoue. Plus sûr qu’un check applicatif (pas de race condition).
Lessons learned
- Cooldown seul = bullshit half-fix : tu reçois quand même les notifs N+1, N+2 après avoir vu la N+0 si la condition perdure
- Le pattern « épisode + ack » est le standard de toute solution d’alerting sérieuse (PagerDuty, OpsGenie, Statuscake…) — pas réservé aux outils pro
- Token random PAR ÉPISODE, pas par device : sinon un ack ancien désactiverait toutes les notifs futures, faille majeure
- UNIQUE INDEX partiel
WHERE ended_at IS NULL: garantie 1-seul-épisode-ouvert au niveau SQL, pas de race condition possible côté appli - Ré-armement : prévoir un bouton « réactiver les notifs » — l’utilisateur ack parfois par erreur, fournir le retour en arrière
- Pré-fetch d’URL en SMS : risque réel selon le client. Pour pousser au-delà du GET-immédiat, demander une confirmation explicite (GET=page, POST=ack)
#alerting #notification #iot #patterns #state-machine #sql #postgresql