Contexte / besoin
Les sites clients en production et les services internes génèrent des événements qui méritent mieux qu’un log perdu : formulaires de contact, webhooks, alertes applicatives. Il fallait un point central qui les reçoive, les persiste et les redistribue, notifications comprises, de façon fiable, y compris quand un destinataire est indisponible ou que le service redémarre.
Particularité de cette pièce : la même personne écrit l’API et l’exploite en production. Chaque choix de développement y est confronté à ses conséquences d’exploitation.
Contraintes
- Livraison garantie. Un événement accepté ne doit jamais se perdre, ni en cas de panne du destinataire, ni lors d’un redéploiement.
- Ingest exposé publiquement, puisque les sites doivent pouvoir pousser depuis Internet : authentification et rate-limiting obligatoires.
- Administration privée. Consultation et gestion n’existent que sur le réseau zero-trust.
Options envisagées
- Un broker dédié (RabbitMQ, Redis avec une file) : la fiabilité de livraison est déléguée, mais c’est une pièce stateful critique de plus à exploiter, pour un débit qui ne le justifie pas.
- Fire-and-forget avec des logs : inacceptable. La perte silencieuse est exactement le problème à résoudre.
- PostgreSQL comme file durable : la base est déjà là, sauvegardée, connue. La file de livraison devient une table, les garanties viennent des transactions.
Décision & pourquoi
Une API NestJS + Prisma + PostgreSQL, avec la file de retry dans la base. Chaque livraison en échec est retentée avec un backoff exponentiel, et l’état de la file survit aux redémarrages puisqu’il est persisté. Le design détaillé de cette file fait l’objet d’un write-up : Concevoir une retry queue persistée.
Le déploiement est single-host à double visage. Le même service expose l’ingest publiquement (rate-limité, authentifié), tandis que les routes d’administration ne répondent que via le tailnet. La frontière se déclare à l’edge (reverse proxy), sans reposer sur le code seul.
Côté exploitation, l’API est traitée comme un service de production à part entière :
- tokens d’API stockés hachés (SHA-256), jamais de credential en clair en base ;
- rate-limiting sur l’ingest public ;
- logs structurés, exploitables plutôt que décoratifs ;
- OpenAPI générée : le contrat est documenté et testable ;
- graceful shutdown : un redéploiement termine proprement ce qui est en cours au lieu de couper des livraisons à mi-chemin.
Tradeoff assumé
PostgreSQL en guise de file ne rivalise pas avec un vrai broker, ni en débit, ni en sémantique avancée (fan-out complexe, consumer groups). À l’échelle réelle du besoin, c’est le bon compromis : une pièce critique de moins, des garanties transactionnelles déjà comprises, et une file lisible en SQL quand il faut diagnostiquer. Si le débit change d’ordre de grandeur, l’interface de publication permet de changer l’implémentation sans toucher au reste.
Résultat
En production depuis mai 2026 : quatre sites y poussent leurs événements. Le volume est aujourd’hui modeste (formulaires et emails transactionnels des sites livrés) ; la mécanique est dimensionnée pour la suite, événements de monitoring et d’exploitation du serveur compris.
- Fiable : pas de perte silencieuse. Ce qui est accepté est persisté, ce qui échoue est retenté, ce qui redémarre reprend où il en était.
- Maintenable : NestJS modulaire, Prisma typé, contrat OpenAPI. La file se lit en SQL.
- Sécurisé : ingest authentifié par tokens hachés et rate-limité, administration inexistante depuis Internet.
Ce que l'ops a changé dans le dev
Exploiter son propre code change la façon de l’écrire. Trois exemples concrets sur le Hub :
- Le graceful shutdown compte. Un déploiement doit passer inaperçu, sans couper de livraisons à mi-chemin qui partiraient en retry. Le service draine avant de s’éteindre.
- Les logs sont structurés parce qu’ils sont lus. Quand on est aussi l’astreinte, un log inexploitable se paie à 23 h. Chaque événement porte de quoi être suivi de l’ingest à la livraison.
- Le rate-limiting protège aussi la base. L’endpoint public est dimensionné pour encaisser un abus sans dégrader le reste de la plateforme.