Aller au contenu
Mohamed Tahiri
Miami --:--
2024 → · perso

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.

Rôle
Backend, recherche, infra
Équipe
Une quinzaine de bénévoles
Volume
1 100+ commits, ~100 PR
Environnements
dev, staging, prod
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

VERCEL · PARISNavigateurSITE · PWAApps mobilesIOS · ANDROIDCloudflareDNS · WAF · ACCÈSFrontNEXT.JS 16 · SSRAPINESTJS 11 · TYPEORMRechercheALGOLIA · PUBLIC SEULSupabasePOSTGRES · AUTH · FICHIERSServicesRESEND · MAPS · PUSHGoogle CloudLOGS · SECRETSSentryERREURS · UPTIMEGitHub ActionsTESTS → MIGRATIONS → PRODTerraformTERRAGRUNT · 14 MODULESAPRÈS COMMITSANS CLÉ · OIDC
Tout ce schéma est décrit en Terraform : trois environnements identiques, dev, staging et prod.

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 :

centers.service.ts
// 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 follows

Un 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 :

terragrunt.hcl
# 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

La base garde ses règles

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.

Aucune migration oubliée

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.

Des tâches planifiées sans serveur

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.

Stack

NestJSTypeORMPostgreSQLSupabaseNext.jsAlgoliaVercelCloudflareTerraformTerragruntGoogle CloudSentryGitHub Actions
Projet suivantSDK, Alexa et Home Assistant →
Contact

Construisons quelque chose.

Ou laissez un message