Auteur : Nicolas Rouanne

Date : 29 janvier 2026


Chez Qraft, nous avons deux produits qui génèrent beaucoup de PDF : Billi, une plateforme SaaS de facturation pour les cabinets de freelances, et Embarq, une société de portage salarial entièrement automatisée. Entre les deux, on génère des factures, des comptes rendus d'activité, des notes de frais, des avoirs, des devis, des contrats et des simulations de paie. Les PDF sont partout.

Pendant des années, on a généré ces documents avec la même stack dans les deux produits. Ça marchait, mais c'était jamais génial. Je veux expliquer pourquoi on a décidé de construire une API dédiée de conversion HTML vers PDF plutôt que de continuer à rafistoler ce qu'on avait.

Le setup : Grover, Rails et Sidekiq

Billi et Embarq sont des applications Rails. Pour la génération de PDF, les deux utilisent Grover, un gem Ruby qui encapsule Puppeteer (qui lui-même encapsule Chromium en mode headless). Le flux ressemble à ça :

  1. Un job en arrière-plan (Sidekiq) prend en charge une tâche de génération PDF
  2. Rails rend un template ERB en chaîne HTML
  3. Grover convertit les URLs relatives en absolues
  4. Grover lance une instance Chrome headless et convertit le HTML en PDF
  5. Le blob PDF résultant est stocké dans ActiveStorage (S3)

Chaque produit a son propre ensemble de classes de service, de templates, de feuilles de style et de jobs. Dans Billi, il y a un concern Pdfable avec une machine à états (draft -> generating -> generated). Dans Embarq, un pattern similaire avec sa propre hiérarchie de classes Pdf::Base.

Ce qui n'allait pas

Le même problème, résolu deux fois. Les deux produits ont essentiellement le même pipeline PDF : HTML en entrée, PDF en sortie, stockage. Mais comme la logique PDF est profondément intégrée dans chaque app Rails, on maintient deux implémentations parallèles. Deux configurations Grover. Deux sessions de débogage CSS. Deux Dockerfiles avec les dépendances système de Chromium.

Chromium est un cauchemar à déployer. À chaque mise à jour d'image de base, quelque chose casse. Grover a besoin de Node.js, Puppeteer et des bibliothèques système de Chromium (GTK, NSS, ALSA, et plus). Sur les Macs Apple Silicon, les développeurs tombent sur spawn Unknown system error -86 et doivent trouver des contournements. La chaîne de dépendances est fragile et ajoute de la complexité à chaque pipeline CI/CD.

Pas de réutilisation du navigateur. Grover crée une nouvelle instance Chrome headless pour chaque PDF. À l'échelle, ça signifie lancer et détruire un processus navigateur complet pour chaque facture. C'est lent et gourmand en mémoire. Pas de pooling, pas de réutilisation.

Le CSS et la pagination sont douloureux. On a passé plus de temps que je ne voudrais l'admettre à débugger des propriétés break-inside, des sauts de page et la fragilité des layouts. L'historique git raconte l'histoire : commit après commit de "review invoice pdf layout", "PDF Company invoice rework", "Adding break-inside properties". Chaque modification de template risque de casser le rendu PDF de manière subtile, et il n'y a pas de boucle de feedback rapide.

De l'asynchrone quand on n'en a pas besoin. L'approche basée sur Sidekiq fait que les PDF sont générés en arrière-plan et stockés. Mais souvent, ce qu'on veut vraiment c'est : envoyer du HTML, recevoir un PDF, terminé. Le modèle asynchrone ajoute de la gestion d'état, de la logique de retry et du coût de stockage pour quelque chose qui pourrait être un simple appel HTTP synchrone.

Ce dont on avait vraiment besoin