Plateforme de cours
Une association veut rendre visibles les cours et les centres de formation près de chez soi. Avec une équipe de bénévoles, je porte depuis 2024 le backend, la synchronisation de la recherche et toute l’infrastructure en code. Premier contributeur du projet.
- commits, premier contributeur
- 1 100+
- modules Terraform
- 14
- environnements identiques, en code
- 3
- versions livrées depuis la v1
- 73
Le produit
Les gens cherchent des cours près de chez eux, et l’information est éparpillée entre des sites vieillissants, des réseaux sociaux et le bouche-à-oreille. La plateforme rassemble les centres, leurs cours et leurs horaires, avec une recherche par lieu, par thème et par niveau. Elle tourne sur le web, en PWA et en apps iOS et Android.
L’architecture
Le front Next.js et l’API NestJS sont déployés sur Vercel, derrière Cloudflare. Les données vivent dans Postgres sur Supabase, qui gère aussi l’authentification et les fichiers. Le front interroge Algolia directement pour la recherche, et retombe sur un moteur Postgres si Algolia ne répond pas.
Ce qui était difficile
Un index de recherche qui ne ment jamais
L’index ne doit contenir que ce qui est public, à jour, et ne jamais faire échouer une écriture en base. J’ai conçu la synchronisation : après chaque écriture, un abonné TypeORM recharge l’entité avec ses relations, la publie si elle est publique et la retire sinon. Par défaut, on cache. Un cours embarque le nom de son centre, donc renommer un centre met à jour tous ses cours. Et chaque nuit, une réindexation complète remplace les collections d’un bloc pour purger les orphelins.
Le piège le plus sournois venait d’ailleurs : TypeORM transmet à l’abonné la charge utile de la mise à jour, pas la ligne. Une mise à jour sans identifiant ne déclenche donc jamais la resynchronisation, sans la moindre erreur. J’ai écrit une règle ESLint qui s’appuie sur le vérificateur de types pour l’interdire :
// TypeORM hands the subscriber the update payload, not the row:
// without an id, the search index is never told and silently goes stale.
await centers.update({ slug }, { isPublic: false })
// ^ lint error: require-id-in-typeorm-update
await centers.update({ id }, { isPublic: false }) // ok: the index followsUn coéquipier a ensuite généralisé le mécanisme : les changements sont mis en tampon par transaction et envoyés seulement après le commit, jamais en cas d’annulation.
Des logs Google Cloud, sans aucune clé
Une fonction Vercel n’a pas d’identité Google, et personne ne voulait d’une clé de compte de service qui traîne. À chaque requête, le logger échange le jeton OIDC de Vercel contre des identifiants Google de courte durée (Workload Identity Federation), puis écrit après la réponse. Cinq destinations de logs ont chacune leur durée de conservation, et des métriques dérivées alimentent un tableau de bord.
Un environnement complet en une ligne
J’ai écrit l’essentiel de l’infrastructure : 82 des 103 commits du dépôt. Terraform et Terragrunt décrivent Vercel, Cloudflare, Supabase, Algolia, Sentry, GitHub et Google Cloud. Une instance du module principal, c’est un environnement entier :
# live/app/prod/terragrunt.hcl: a whole environment in one call
include "root" { path = find_in_parent_folders("root.hcl") }
terraform { source = "../../../modules/app" }
inputs = {
environment = "prod"
# two Vercel projects, DNS, Maps keys, search indexes, secrets,
# log sinks, dashboards, uptime monitors and access policies
}La CI s’authentifie sans clé et ne lit que les secrets dont elle a besoin. Chaque jour, un plan détecte toute dérive entre le code et la réalité, et chaque PR reçoit en commentaire le résumé de ce qui changera.
Livrer sans casser
Contraintes CHECK, index partiels, tableaux d’enums plutôt que tables de jointure. Les dates d’inscription sont des `date`, plus des `timestamptz` : fini le jour qui glisse selon le fuseau.
Avant chaque déploiement, la CI applique les migrations puis tente d’en générer une nouvelle. Si quelque chose sort, le build échoue et affiche le diff.
Les crons sont des routes HTTP appelées par Vercel, protégées par un secret comparé en temps constant. Chaque job est idempotent et journalisé.
Ce que j’en retiens
Porter un produit avec des bénévoles, c’est rendre la contribution facile : une règle de lint qui attrape un piège vaut mieux qu’une page de documentation. Et une infrastructure en code, c’est la seule documentation qui ne vieillit pas.