Movie Picker aide un groupe à choisir un film sans y passer la soirée : chacun propose, tout le monde vote, une roue tranche. Je l'ai commencé en février 2026 pour un besoin réel dans mon entourage, et ça ne devait pas dépasser le stade du petit outil. C'est devenu une plateforme, puis mon projet de fin de master, et le service est aujourd'hui en ligne, ouvert au public, maintenu par moi seul.
Il me sert aussi de bac à sable : j'y essaie des technologies et des méthodes, j'explore, je lis les documentations. Le but n'est pas d'en faire le projet d'une vie, mais d'y gagner de l'expérience et de prendre plaisir à faire vivre un produit qui évolue avec les retours de ses utilisateurs, en couvrant toute la chaîne, du besoin métier à la mise en production.
169 000lignes de codeFichiers .ts, .tsx et .css du front plus les .cs du serveur, comptés au build et arrondis au millier.
84endpoints HTTPAttributs [HttpGet], [HttpPost] et suivants, comptés dans les contrôleurs du serveur.
3 753tests automatisésCas de test déclarés dans le dépôt, comptés au build : 1718 côté front et 2035 côté serveur, répartis sur 464 fichiers.
1 250commitsCommits de la branche principale depuis le premier jour du projet, comptés au build.
6 moisde développementMois complets écoulés depuis le tout premier commit du dépôt, calculés au build.
86 %de couverture exigéePart de lignes que les tests du front doivent couvrir. En dessous de ce seuil, la chaîne d'intégration échoue et rien ne part en production.
Document mis à jour le 2026-09-12, chiffres relevés au build du 2026-09-20
01 / Architecture
Deux applications, une frontière nette
Un front statique sur CDN, une API conteneurisée, une base managée. Ce qui compte : la frontière entre les deux est un contrat, pas une habitude.
Le trait bleu est le seul chemin entre le front et le serveur. Les flèches en pointillés vont dans le sens de l'appel.
Comment les deux moitiés tiennent ensemble
FrontièreAucun accès direct à la base depuis le navigateur : tout passe par ces routes, et le contrat sert de frontière vérifiable.
Le front ne connaît du serveur que 84 routes sous /api/v1, décrites par un contrat que la chaîne rejoue à chaque envoi.
Aucun état en mémoireLes clés qui signent les cookies sont rangées en base, ce qui évite de déconnecter tout le monde à chaque déploiement.
L'hébergeur peut lancer plusieurs instances du serveur, alors rien ne vit dans le processus : ni session, ni minuterie, ni tâche de fond. Seule exception, le cache des fiches TMDB, propre à chaque instance et sans effet sur ce qui est renvoyé.
Deux déploiementsDeux étapes distinctes dans la chaîne : une correction de style ne redéploie pas le serveur.
Le front part sur le CDN, le serveur part en image conteneurisée. Chacun peut sortir sans attendre l'autre.
Les services extérieurs branchés au serveur
Aucun n'est indispensable au fonctionnement : chacun a un comportement défini quand sa clé manque ou quand il ne répond pas, et le serveur démarre sans eux.
TMDBThe Movie Database, la base ouverte qui fournit titres, résumés, genres et affiches.
Métadonnées, genres et affiches. Les fiches et les affiches sont mises en cache, et une panne est retenue quelques minutes plutôt que réessayée à chaque requête.
LetterboxdRéseau social de cinéphiles : la liste publique d'un profil est lue puis rapprochée du catalogue.
Import de watchlist depuis le profil public. Sans API publique, la page est lue telle quelle : 300 films au plus, une fois par 24 heures et par compte.
Google, GitHubDélégation d'identité : le mot de passe reste chez le fournisseur, l'application ne reçoit qu'un jeton.
Connexion déléguée. Un fournisseur non configuré disparaît de l'écran au lieu d'échouer, et la connexion par mot de passe reste disponible.
Web PushVAPID signe chaque notification pour prouver au navigateur quel serveur en est à l'origine.
Notifications navigateur, protocole VAPID. Un abonnement que le navigateur déclare expiré est effacé de la base au premier envoi qui le rencontre.
ResendService d'envoi transactionnel : le courriel part par un appel d'API plutôt que par un serveur de messagerie à administrer.
Envoi des courriels de réinitialisation de mot de passe. Sans clé configurée, le message part dans les journaux du serveur au lieu de faire échouer le démarrage.
Ko-fiPlateforme de dons pour créateurs ; le soutien confirmé apparaît ensuite comme un badge sur le profil.
Dons. C'est Ko-fi qui appelle le serveur : l'appel porte un jeton partagé, il est limité en débit, et il est refusé si le jeton ne correspond pas.
Cloud SchedulerPlanificateur de Google Cloud : il appelle une adresse à l'heure dite, ce qui remplace une minuterie vivant dans le serveur.
Appelle le serveur à heure fixe : toutes les 30 minutes pour les rappels de soirée, une fois par jour pour les soirées récurrentes. Même protection par jeton, puisque les routes sont ouvertes sur internet.
GitHub IssuesLe suivi de bugs de GitHub, alimenté directement par le formulaire de suggestion de l'app.
Une suggestion envoyée dans l'app ouvre une issue, capture d'écran comprise. Sans jeton, l'envoi est ignoré et journalisé plutôt que renvoyé en erreur.
02 / Choix techniques
Ce qui a été arbitré, et ce qui a été hérité
Certaines de ces décisions ont été prises contre une alternative nommée. D'autres viennent de l'histoire du projet et sont gardées en connaissance de cause.
.NET 10 côté serveurLes routes et les charges utiles n'ont pas changé : dans le commit de migration, les seules lignes touchées côté front sont du reformatage, pas un appel.
Le serveur était en Node et Express jusqu'en mars 2026, puis réécrit en ASP.NET Core à contrat identique. Typage à la compilation, injection de dépendances native, xUnit.
Le prix Une réécriture complète du serveur, trois semaines après le premier commit.
MongoDB plutôt que PostgreSQLLes transactions multi-documents restent disponibles : le cluster tourne en replica set, condition nécessaire pour les ouvrir.
Le document d'une soirée est lu en bloc et sa forme change à chaque palier. Un moteur documentaire évite une migration de schéma par fonctionnalité.
Le prix L'intégrité référentielle est portée par le code et par des index uniques, pas par le moteur.
Driver MongoDB plutôt qu'un ORMUn ORM documentaire réintroduit un schéma là où le moteur n'en impose pas, et masque la requête réellement envoyée.
L'API en Node passait par Mongoose ; la version .NET parle au driver directement. Chaque collection a sa classe de document, aux noms de champs explicites, et la conversion vers le domaine est écrite à la main.
Cookie de session plutôt que JWTUn jeton gardé en mémoire ou en stockage local se lit depuis JavaScript ; un cookie HttpOnly ne se lit pas, ce qui retire une cible au vol de session.
Le navigateur reçoit un cookie signé, marqué HttpOnly et SameSite, jamais un jeton à ranger quelque part. Les clés de signature vivent en base.
Le prix Le serveur doit répondre sous le même domaine parent que le front, et chaque appel emporte le cookie.
React et Vite plutôt que Next.jsLe rendu serveur aurait ajouté une infrastructure à tenir pour un catalogue de pages publiques restreint : l'accueil, les profils, le dossier technique et les pages légales.
Une application entièrement cliente, livrée en fichiers statiques : aucun serveur de rendu à exploiter, à mettre à échelle ni à payer.
Le prix Cinq pages publiques sont pré-rendues au build, HTML complet et métadonnées compris ; l'accueil et les profils, dynamiques, restent référencés par métadonnées et sitemap.
Modules CSS plutôt que TailwindLe contrôle échoue sur une valeur écrite en dur, un z-index nu ou un point de rupture hors échelle. Il tourne avant chaque envoi et dans l'intégration continue.
Des modules CSS et des jetons maison : espacements, tailles, couleurs et profondeurs forment une échelle fermée que le contrôle refuse de voir contournée.
Cloud Run plutôt que KubernetesKubernetes demandait un outillage disproportionné pour un service ; une machine virtuelle demandait un OS à patcher.
Un conteneur, une mise à l'échelle jusqu'à zéro, une facturation à la requête. Aucun système d'exploitation à tenir à jour.
Le prix Aucun état en mémoire n'est fiable et aucun travail de fond ne peut vivre dans le processus. Les rappels sont déclenchés de l'extérieur.
Un seul fournisseur de cloud, depuis septembre 2026Cloud Storage et Cloud CDN, l’équivalent direct de S3 et CloudFront, ont été écartés : leur règle de transfert coûte près de vingt dollars par mois avant le premier octet servi. Firebase Hosting sert les fichiers, les en-têtes et le repli SPA dans le palier gratuit.
Le front est né sur AWS avant que le serveur ne parte sur Google Cloud ; il l’y a rejoint sur Firebase Hosting. Une console, un modèle de droits, une facture.
Le prix Un hébergement moins programmable qu’un CDN : sans fonction devant le site, un chemin inconnu reçoit la coquille en 200 et l’aperçu de partage ne peut pas être servi selon le client.
Monorepo pnpm et TurboTurbo met en cache les tâches par graphe de dépendances : seul ce qui a changé est reconstruit.
Un dépôt, une chaîne de livraison, un seul tag de version pour les deux applications. Le contrat d'API et le client qui le consomme changent dans le même commit, donc une rupture ne compile pas au lieu de se découvrir en production.
03 / Le contrat
La frontière est un fichier, pas une convention
Le contrat OpenAPI est exporté depuis le serveur à chaque construction. Le front garde ses propres types, et un test au niveau des types vérifie qu'ils ne lisent aucun champ absent du contrat.
Du code serveur au contrat, du contrat aux types du front, et le test qui casse la CIAPI .NETcontrôleurs et DTOopenapi-v1.jsonexport automatiqueSchéma généréopenapiSchema.tsTest de contratun type du front lit-ilun champ inexistant ?Si oui, la CI échoueUne vérification régénère les types et échoue s'ils ont dérivé.
Export du contratLe script openapi:export démarre l'application et écrit le contrat dans artifacts/openapi-v1.json.
Le serveur sérialise son OpenAPI depuis les contrôleurs ; le fichier est un artefact de build, pas un document tenu à la main.
Types du front confrontésopenapi-typescript produit openapiSchema.ts, importé par ce seul test : le schéma sert de référence, pas de source pour le code applicatif.
Le front garde ses propres types, écrits pour son usage. Un test les confronte au contrat et refuse de compiler si l'un d'eux lit un champ que le serveur n'expose pas.
Détection de dériveopenapi:types:check compare la sortie fraîche au fichier commité ; apiContract.test.ts vérifie en plus, au niveau des types, que les champs lus par le front existent bien.
La CI régénère les types et échoue si le fichier versionné ne correspond plus au contrat exporté.
Deux filets de portée différente. Le premier est complet : les 54 routes appelées par le front sont listées une à une, et l'une qui disparaît du contrat fait échouer la compilation. Le second va plus loin mais couvre moins : il confronte champ par champ 15 réponses sur les 51 du contrat, celles dont le front déclare son propre type. Les autres, il les lit sans leur donner de nom, et certaines ne le concernent pas, comme les sondes de santé. Les couvrir demande d'abord de nommer ces types : du travail mécanique, pas une décision à prendre.
04 / Interface
Découpé par usage, pas par type de fichier
7 domaines autonomes, un socle partagé qui n'en connaît aucun. Vérifié mécaniquement : le socle est une feuille du graphe, aucun cycle toléré.
Graphe d'imports du front : sept domaines qui pointent tous vers le socle partagéGRAPHE D'IMPORTS DU FRONTauthsoiréesfilmswatchlistnotificationsprofilsLetterboxdshared/ : 37 composants, hooks, client HTTPne connaît aucune featureSOCLE TECHNIQUEReact 19TypeScript 6Vite 8React Router 8TanStack Query 5
Le socle est une feuille du graphe : aucune flèche n'en repart. Un cycle fait échouer la CI.
DesignUn jeton est une variable CSS unique pour une couleur, un espacement ou une taille de texte. Un survol laissé libre reste collé après un appui sur mobile, d'où la condition.
Jetons fermés : aucune valeur littérale acceptée en CSS, et tout état de survol confiné aux appareils qui en ont un.
Données serveurTanStack Query range chaque réponse sous une clé, sert le cache pendant qu'il rafraîchit en arrière-plan, et expose les états de chargement et d'erreur au composant.
Un cache client tient les réponses de l'API. Une écriture invalide les vues concernées au lieu de recharger la page.
Chargement à la demandeLe premier écran n'emporte pas le reste de l'application, et les paquets de dépendances restent en cache du navigateur d'une version à la suivante.
23 écrans téléchargés seulement quand on y va, et les dépendances rangées à part du code applicatif.
Hors ligneLe service worker sert la coquille de l'app et les affiches déjà vues quand le réseau manque.
Installable, coquille et affiches en cache, consultable sans réseau.
LanguesLes textes viennent d'un dictionnaire par langue, vérifié à la compilation contre les clés du français.
Français et anglais, bascule sans rechargement.
Accessibilitéaxe-core rejoue les règles WCAG sur le rendu de chaque vue pendant la suite de tests.
Audit automatisé sur 27 vues à chaque exécution des tests.
05 / Serveur
Le métier ne connaît ni la base ni le web
Quatre couches, une seule règle : tout pointe vers l'intérieur. Vérifiée à chaque exécution de la CI, pas seulement écrite.
Les quatre couches de l'API et le sens des dépendancesCONTROLLERS15 contrôleurs, validation, codes HTTPINFRASTRUCTUREMongo, TMDB, Letterboxd, push, e-mailAPPLICATION75 cas d'usage, 30 portsDOMAINentités, règles, aucune dépendanceLes dépendancesne pointent quevers l'intérieur.Un import interditfait échouer la CI.
30 ports, 14 dépôts implémentés deux fois : en mémoire et sur MongoDB.
Le trajet d’une requête
Le trajet d’une requête à travers les quatre couches, et le sens des dépendancesPOST /api/v1/events/{slug}/moviesentréepipeline HTTPorigine autoriséesession et identitélimitation de débitControllerscontrôleurroute et cas d’usagevalide la formeaucune règle métierApplicationcas d’usageorchestre le casouvre la transactionports uniquementDomainrègles métierentités et règleszéro dépendancetestable sans rienInfrastructureadaptateurimplémente un portparle à MongoDBdoublé en mémoireles ports30 interfaces dans Application, 75 cas d’usage derrière.Une flèche ne pointe jamais vers l’extérieur : le contrôle d’architecture fait échouer la construction si elle le fait.
Les mêmes 75 cas d’usage servent les tests unitaires et la production : ce qui change, c’est l’implémentation branchée derrière les ports.
Ce qui tient la frontière
Cloisons vérifiéesCe ne sont pas des consignes : le contrôle lit les using de chaque fichier et fait échouer la construction au premier écart.
Le domaine ne peut importer que System et lui-même. L'application ignore MongoDB, le framework web et l'infrastructure. Un contrôleur ne touche jamais la persistance.
La version en un seul endroitUne route qui écrirait son préfixe à la main pourrait diverger du contrat sans que rien ne le signale.
Le préfixe /api/v1 est une constante unique, jamais recopiée dans une route. Le contrat exporté et les types du front en héritent.
Deux sondes de santéL'hébergeur distingue ainsi une instance qui démarre d'une instance qui ne peut pas servir.
/health répond aussitôt qu'un processus est vivant. /health/ready interroge la base et renvoie 503 si elle ne répond pas, avec la durée mesurée et la version déployée.
06 / Modèle de données
Neuf collections au cœur, et des écritures tout ou rien
L’intégrité ne vient pas du moteur : elle vient des index uniques posés au démarrage et des transactions ouvertes pour toute écriture qui touche plusieurs documents.
Modèle de données : les collections du cœur métier et les index uniques qui les tiennentusersemail uniquehandle uniqueidentités OAuthwatchlistuser + film + typeuniquefollowssuiveur + suiviuniqueuser_notificationsuser + dateexpire à 90 joursparticipantssoirée + userpseudo uniqueeventsslug uniqueréglages de soiréemoviessoirée + film TMDBuniquevotessoirée + film + votantun vote chacunseen_markssoirée + film + votantune marque chacun
Les traits sont les identifiants qui relient les documents. Users et Events sont encadrés parce que tout le reste s’y rattache : chaque autre document porte l’un ou l’autre. 19 collections au total, 41 index déclarés au démarrage, dont 8 à expiration automatique.
Suppression de compte : huit collections écrites dans une transaction, validées ou annulées ensembleUNITÉ DE TRAVAILDELETE /auth/meIUnitOfWork.ExecuteAsyncuserseventsmoviesvoteswatchlistsnotificationsfollowssessionshuit collections touchéescommitles huit écrituresrollbackaucune des huitTransaction MongoDB en production, verrou en mémoire dans les tests : le même code des deux côtés.
Supprimer un compte touche huit collections dans une seule transaction, dont deux anonymisées plutôt que vidées : aucun état intermédiaire n'est observable.
UnicitéUn moteur documentaire ne connaît pas les clés étrangères : ces index sont ce qui empêche deux comptes de partager un e-mail ou un participant de voter deux fois.
14 index uniques refusent les doublons dans le moteur : un e-mail, un pseudo, un lien de soirée, un vote par participant et par film.
AtomicitéUne transaction regroupe plusieurs écritures : soit toutes aboutissent, soit aucune. Les soirées créées et les participations restent, l'identité en moins, pour ne pas amputer les données des autres participants.
Toute écriture groupée passe par une unité de travail, et ne peut pas s'arrêter à mi-chemin.
Expirations automatiquesAucune tâche de fond ne tourne pour ça, ce qui tombe bien puisque rien ne vit dans le processus du serveur.
8 index à expiration font le ménage dans le moteur : sessions, jetons de mot de passe, notifications, marqueurs anti-doublon, compteurs de limitation et journal des dons.
Inventaire des indexTrois garanties ne tiennent qu'à ces index : l'expiration des sessions, celle des jetons de réinitialisation et celle des compteurs de limitation de débit.
Un test compare les 41 index réellement créés à une liste attendue : nom, unicité et expiration. En ajouter un sans le déclarer casse la construction.
MigrationsIdempotente veut dire qu'une deuxième exécution ne change plus rien au résultat de la première.
Datée, idempotente, appliquée une fois au démarrage puis consignée. 4 en production.
IsolationLe garde-fou compare le nom de la base à l'environnement et refuse de démarrer en cas de mélange.
Bases dev et prod séparées, avec refus de démarrer si un environnement vise la mauvaise.
AffichesLes images sont copiées côté serveur : un changement d'URL chez le fournisseur ne casse plus l'affichage.
Récupérées une fois puis servies depuis le serveur. Chaque consultation repousse leur expiration ; sans accès pendant 30 jours, elles sortent du cache.
07 / Une fonctionnalité
Une fonctionnalité de bout en bout
La synchronisation Letterboxd traverse toutes les couches : un navigateur, un service tiers sans API publique, un rapprochement de catalogue, une écriture. Et un cas où la machine refuse de décider seule.
Synchronisation Letterboxd : du clic à l’écriture, avec le retour vers l’utilisateur quand le titre est ambigunavigateurserveurextérieurbaseouvertureune fois par sessionplafond1 par 24 h, autolecture publiquepas d’API, 300 maxrapprochementTMDB, année ± 1 anécartsajouts et retraitsécriturejamais de doublonarbitragel’utilisateur trancheUne lecture incomplète annule toute la synchronisation : mieux vaut ne rien écrire que vider une liste.
Un seul chemin d'écriture, et un arbitrage renvoyé à l'utilisateur quand la correspondance n'est pas certaine.
DéclenchementLe plafond régule le déclenchement automatique, pas le geste volontaire de l'utilisateur ; c'est la limitation de débit de la route qui borne les appels répétés.
Automatique à l'ouverture de l'application, avec un plafond serveur d'une fois par 24 heures. Une synchronisation demandée explicitement, à la connexion du compte ou par le bouton, passe outre ce plafond.
LectureSi une page manque, appliquer le résultat viderait la liste de l'utilisateur : le code préfère ne rien écrire.
Letterboxd ne publie pas d’API. La watchlist publique est parcourue page par page, et une lecture incomplète annule toute la synchronisation.
RapprochementJusqu’à cinq candidats sont examinés ; la correspondance n’est retenue que si exactement un titre est identique après normalisation.
Chaque titre est cherché dans TMDB avec une tolérance d'un an sur l'année, puis comparé au titre original comme au titre traduit du candidat, accents et casse retirés.
AlignementLe slug est gardé sur l'élément de watchlist : c'est lui qui permet à la synchronisation suivante de calculer un écart au lieu de tout ré-importer.
La synchronisation aligne les deux listes : elle ajoute les films absents et retire ceux qui ont quitté Letterboxd. La rejouer ne crée aucun doublon, l'index unique et le slug conservé font foi.
ArbitrageDeux films au même titre, ou un titre traduit différemment : le code sait qu’il ne sait pas, et le dit.
Un titre ambigu remonte à l’utilisateur avec ses candidats au lieu d’être deviné. Rien n’est écrit tant qu’il n’a pas tranché.
TestsLe bouchon a révélé un vrai défaut : le cache n’était pas invalidé après un arbitrage manuel.
Le client tiers est doublé par un bouchon en test de bout en bout, sinon la suite dépendrait de la disponibilité d’un site externe.
08 / Tests
3 753 tests automatisés, et une couverture qui bloque
Trois familles, de la plus rapide à la plus lente : le domaine sans dépendances, le serveur contre une vraie base, le produit dans un navigateur. Chacune répond à une question que les autres ne posent pas.
Pyramide de tests : unitaires, intégration, end to end36 tests, 9 fichiersEND TO END202 tests, vraie MongoDB en replica setINTÉGRATION3 551 tests : 1 718 interface, 1 833 serveurUNITAIRE
Couverture bloquante côté front : 86 % de lignes, 81 % de fonctions, 77 % de branches. Côté serveur aussi : 90 % de lignes sur la suite unitaire, et 86 % sur les seuls adaptateurs Mongo, que seule la suite branchée sur une vraie base exécute.
Tests unitairesIls couvrent le domaine, les cas d'usage et les composants d'interface isolés de leurs dépendances.
3 551 cas, sans base ni réseau, sur les doublures en mémoire. Quelques secondes.
Tests d'intégrationC'est le seul chemin qui exécute réellement les adaptateurs Mongo et les transactions ; la CI le rejoue dans un job dédié.
202 cas rejoués contre une vraie MongoDB, transactions comprises.
Tests end to endLa persistance y est l'implémentation en mémoire et les services tiers sont bouchonnés : le parcours est vrai, l'environnement reste déterministe.
36 cas Playwright qui pilotent Chromium contre le front compilé et le serveur démarré.
Comment ils sont écrits
Le test avant le codeSur un bug, le test doit reproduire le symptôme avant la correction, sinon rien ne prouve que la cause a été traitée.
Fonctionnalité comme correctif : le test qui décrit le comportement attendu est écrit et échoue avant l’implémentation.
Doublures en mémoireCe n’est pas une bibliothèque de simulacres : ce sont de vraies implémentations, tenues par les mêmes interfaces.
14 dépôts implémentés deux fois : sur MongoDB pour la production, en mémoire pour les tests unitaires.
Serveur réel en testWebApplicationFactory monte le vrai pipeline ASP.NET Core ; seule la base et les services tiers sont substitués.
La suite d’intégration démarre l’application entière en mémoire et l’interroge en HTTP, contre une MongoDB en conteneur.
MongoDB en replica setChaque classe de test reçoit sa propre base jetable : les tests ne se marchent pas dessus.
Le conteneur de test est créé à la volée en replica set, sinon les transactions ne pourraient pas être testées.
Services tiers bouchonnésSans cela, la suite dépendrait de la disponibilité et des quotas de deux sites externes. L'envoi d'e-mail et les issues GitHub, eux, tombent sur leur repli faute de clé configurée.
TMDB et Letterboxd sont remplacés par des bouchons que deux variables d’environnement activent.
Réseau simulé côté frontLes composants appellent l'API normalement : c'est la couche réseau qui est interceptée, pas le code de l'application.
42 fichiers de test montent un serveur HTTP simulé et répondent aux vraies requêtes des écrans, sans jamais sortir sur le réseau.
09 / Mesures
Ce qui est mesuré, et le seuil qui fait échouer
Une métrique sans seuil est une décoration. Les quatre premières arrêtent une livraison ; les trois dernières racontent ce qui se passe une fois en ligne.
Seuils bloquants
Couverture de testsLes seuils sont montés au fil des paliers ; ils ne descendent jamais, c’est ce qui les rend utiles.
86 % de lignes, 81 % de fonctions, 77 % de branches côté front. Côté serveur, 90 % de lignes sur la suite unitaire et 86 % sur les seuls adaptateurs Mongo. En dessous, la suite échoue.
LighthouseChaque page est mesurée cinq fois et c'est la médiane qui est retenue, pour lisser la variance du runner. Plus aucune page n'a de plancher à elle depuis que l'état déconnecté d'une route protégée se rend depuis la coquille.
14 pages auditées à chaque envoi, avec des minimums de 85 en performance, 100 en accessibilité, 100 en bonnes pratiques et 95 en référencement.
Accessibilité automatiséeaxe rejoue les règles WCAG sur le rendu réel de chaque vue, y compris l’écran d’erreur.
27 vues passées à axe-core pendant la suite de tests. Une violation fait échouer le test, pas un rapport.
SonarCloudLes deux déploiements dépendent de ce job : un portail rouge arrête la livraison, il ne se contente pas de l’annoter.
Analyse à chaque envoi, couverture ingérée depuis la CI. La chaîne attend le verdict du portail qualité et échoue s’il est rouge.
Le contrôle d'architecture est un script maison d'environ 830 lignes, joué avant chaque push et en CI. Il fait échouer la construction sur cinq familles de fautes qu'aucun linter du marché ne sait interdire :
un commentaire hors directive fonctionnelleRejette // et /* */, sauf les directives comme @ts-expect-error, eslint-disable ou un shebang.
une valeur littérale en CSSRejette un espacement ou une couleur écrits en dur dans un module CSS au lieu du jeton correspondant.
un point de rupture hors échelleSeule une liste fermée de largeurs est admise ; toute autre valeur fait échouer la construction.
un import interdit entre couchesRejette un import du socle partagé vers un domaine, ou du métier vers la base et le web.
un cycle d'importsRejette deux modules qui finissent par s'importer l'un l'autre, directement ou par un détour.
Ce qui est observé en production
Cloud MonitoringLes sondes visent /health, /health/ready et la racine du front. Les seuils sont posés au-dessus du bruit mesuré pour qu'une alerte reste crédible, et un incident se referme seul après trente minutes de retour à la normale.
Trois sondes interrogent le service de l'extérieur, depuis trois continents, et cinq politiques d'alerte préviennent par courriel : service ou base injoignables, erreurs serveur, latence dégradée.
SentryActif en permanence en production, sans donnée personnelle : c’est de la surveillance de service, pas de la mesure d’audience.
Erreurs du navigateur et du serveur, rattachées à la version déployée par le SHA du commit.
PostHogLes événements émis avant le choix de consentement sont perdus, et c’est le comportement voulu.
Événements produit et Web Vitals mesurés chez de vrais visiteurs, uniquement après consentement explicite.
10 / Intégration continue
19 checks de CI avant la production
Un seul graphe, des dépendances explicites, un déploiement qui n'a lieu que si tout ce qui le précède est vert.
Graphe des jobs d'intégration continue, du déclenchement au déploiementpush / PRchangesvers l’imagelint apitest api mongotest apivers l’image et le frontgitleakslint workflowsaudit depsvers le fronttest weblint weblighthousevers les deux déploiementse2ee2e mongoimage APIsonardeploy apideploy frontdeployguard
Le filtre de périmètre décide quelles branches tournent ; gitleaks et lint workflows lui échappent et tournent à chaque fois. Un seul job réunit navigateur réel et vraie base, e2e mongo, et il bloque les deux déploiements. Le dernier maillon vérifie que les déploiements ont eu lieu, pas seulement qu'ils n'ont pas échoué.
Ce qui tient la chaîne
DéclenchementLes branches ouvertes par le robot de dépendances sont exclues du déclenchement par poussée, pour ne pas jouer la chaîne deux fois.
Toute poussée et toute demande de fusion vers master lancent la chaîne. Le déploiement, lui, ne part que depuis master.
Périmètre calculéC'est ce qui garde la chaîne courte, et c'est aussi ce qui a rendu le garde-fou de déploiement nécessaire.
Un premier job compare les fichiers modifiés et décide quelles branches du graphe tournent. Une correction de style ne rejoue pas la suite serveur.
Recherche de secretsIl tourne sur l'arbre de travail et bloque la construction de l'image ; la plateforme refuse en plus un push qui contiendrait une clé reconnue.
Un balayage cherche clés et jetons dans le code avant la construction de l'image. Une correspondance arrête tout.
Image étiquetéeLe déploiement pointe une image précise plutôt qu'une étiquette mouvante : revenir en arrière consiste à repointer la précédente.
Le serveur part en image conteneurisée, étiquetée par l'empreinte du commit et poussée dans le registre avant tout déploiement.
Garde-fou de déploiementIl existe parce que le cas s'est produit : des jobs sautés laissaient la chaîne verte alors que la production était à moitié à jour, un job sauté n'étant pas un job en échec.
Un dernier job compare le périmètre modifié au résultat de chaque déploiement et échoue si le front a changé sans partir en production.
Chaîne mise en cacheChaque job porte aussi un délai maximal, pour qu'une étape bloquée ne retienne pas la chaîne indéfiniment.
Dépendances NuGet, tâches Turbo, couches Docker et base de vulnérabilités sont conservées d'une exécution à l'autre.
Trois autres chaînes, hors du graphe principal
retour arrièreDéclenchable à la main pour remettre en ligne l'image précédente.
nettoyage du registrePurge les anciennes images Docker pour que le registre ne gonfle pas indéfiniment.
analyse de sécuritéPlanifiée, indépendante des envois de code, pour attraper les failles publiées après coup.
11 / Infrastructure
Où ça tourne, et ce qui a le droit de le changer
Rien n’est déployé à la main. Une image par commit, des secrets hors du dépôt, un retour arrière en une exécution.
Infrastructure : le front et le serveur sur Google Cloud, la base managée à partGoogle Cloud, europe-west1Cloud Monitoringsondes front et APIsix alertes activesFirebase Hostingwww.movie-picker.frTLS géré, en-têtesSecret Managerclés et connexionsinjectés au déploiementCloud Runconteneur, échelle à zéroorigines vérifiéesArtifact Registryune image par committaguée par SHA, purgéeCloud Schedulerrappels, soirées récurrentes et terminéescréé si le jeton existeMongoDB Atlasreplica set managétransactions disponiblesSentryerreurs front et serveurrégion européenneRegistre, secrets, Cloud Run et Hosting décrits en Terraform ; identités, alertes et recette restent à décrire.Le pointillé n'est pas un chemin réseau : le navigateur appelle le serveur directement.L'origine du front est la seule que le serveur accepte.
Le front et le serveur vivent chez le même fournisseur, en europe-west1, reliés par une seule origine autorisée.
Déploiement constatéSe terminer sans erreur ne prouve pas qu'un service répond ; ces appels le prouvent.
Après chaque déploiement, la chaîne interroge les deux sondes de santé du serveur et charge le front sur son domaine public. Un service muet fait échouer le déploiement.
ImageLe tag est le SHA du commit : une version en production est toujours rattachable à une ligne de code exacte.
Une image Docker par commit, taguée par son empreinte Git et poussée dans un registre privé.
SecretsUne origine manquante fait échouer le déploiement ; un secret optionnel absent se signale par un avertissement et désactive la fonctionnalité qui en dépend.
13 clés et chaînes de connexion vivent dans Secret Manager, injectées au déploiement, jamais dans une image ni dans le dépôt.
Retour arrièreLes révisions restent disponibles chez l’hébergeur et les anciennes images dans le registre, purgées par sa politique de rétention pour qu’il ne gonfle pas.
Un déclenchement manuel bascule tout le trafic du serveur vers la révision précédente, déjà en ligne : aucune reconstruction, aucun redéploiement. Le front a le sien : l'hébergement garde chaque version publiée, et un déclenchement remet la précédente en service.
Travail périodiqueLes deux jobs sont créés par la chaîne de déploiement, mais seulement si le jeton existe : sans lui, ni rappel ni occurrence suivante ne partent, et le déploiement le signale par un avertissement.
Aucune tâche de fond ne vit dans le processus. Un planificateur externe appelle le serveur toutes les 30 minutes pour les rappels de soirée, et une fois par jour pour faire naître l'occurrence suivante des soirées récurrentes, sur des routes protégées par jeton.
OriginesLa liste est une variable de dépôt, contrôlée avant l’appel de déploiement, pas une valeur par défaut permissive.
Le serveur n’accepte que les origines déclarées. Le déploiement échoue si la liste n’est pas renseignée.
12 / Sécurité
Ce qui protège la production
Chaque garde-fou est rattaché à un mécanisme précis plutôt qu'à une intention.
Mots de passeLe sel rend deux mots de passe identiques indiscernables en base, et les itérations rendent une attaque par force brute coûteuse.
Jamais stockés, seulement leur empreinte salée issue d’une dérivation à itérations. Un format devenu obsolète est réencodé à la connexion suivante.
DépendancesUne CVE est une faille publiée avec un identifiant public ; l'image est scannée avant publication.
Audit npm et NuGet à chaque envoi : une faille haute ou critique arrête la chaîne. L'image est scannée avant publication, les mises à jour sont automatisées.
NavigateurLa politique de sécurité du contenu liste les origines autorisées et bloque tout le reste.
Politique de sécurité du contenu, en-têtes de protection, origines déclarées.
SessionsSans clés persistées, chaque déploiement changerait la signature des cookies et déconnecterait tout le monde.
Clés de signature persistées en base, pour qu'un déploiement ne déconnecte personne.
AbusLa limitation plafonne le nombre d'appels par adresse sur les routes de connexion et d'écriture.
35 politiques de limitation de débit, une par famille de routes sensibles.
DémarrageMieux vaut une panne visible au démarrage qu'un service qui accepte n'importe quelle origine ou qui tourne sans persistance.
Hors développement, le serveur refuse de démarrer sans liste d'origines autorisées ni adresse de base de données.
RGPDExport et suppression se déclenchent depuis la page de compte, sans passer par une demande écrite.
Export et suppression de compte en autonomie, consentement avant tout script tiers.
TracesL'utilisateur qui signale une erreur porte sans le savoir la clé qui retrouve sa trace exacte côté serveur.
Journaux structurés et identifiant de requête, propagé jusqu'aux journaux et renvoyé dans chaque réponse d'erreur.
13 / Méthode de travail
Une exécution assistée, des décisions qui ne le sont pas
Ce projet est développé avec un assistant IA. Le principe tient en une phrase : je délègue l'exécution, jamais la décision, et je rends la vérification automatique plutôt que déclarative.
Flot produit : les signaux de production alimentent le brainstorming, qui alimente la roadmapFLOT PRODUITmonitoring et retours terrainSentry, PostHog, GitHub Issuesbrainstormingalimente le backlogroadmappriorisationconversation avec un modèle frontièreil lit la doc et le code avant de répondreun besoin priorisé part ensuite dans le flot de développement
Le brainstorming alimente le backlog, la roadmap le priorise. Chaque étape se mène en conversation avec un modèle frontière qui a lu la documentation et le code du dépôt avant de répondre.
Flot de développement en quatre phases, du besoin au déploiementFLOT DE DÉVELOPPEMENT01spécificationfeature de la roadmapcadrage fonctionnelcompte rendu fonctionnelmaquette si besoin UI02implémentationplan de testsdéveloppement en TDDcontrôle en navigateurrecette locale03revuecode review, sécuritécorrectionsroadmap mise à jourverify:local avant push04livraisonpush, branche de versionCI, 19 checksmerge en fin de versionmise en prod manuellemonitoringSentry, PostHogles signaux réels réalimentent le backlog produitLes quatre étapes encadrées attendent ma relecture et mon accord avant de continuer.
Le compte rendu fonctionnel fixe les cas à la marge, chacun devient un test avant le code, et aucune ligne n'est envoyée avant que j'aie testé la fonctionnalité moi-même. Ce que la production révèle repart ensuite vers le backlog produit.
Traitement d'une anomalie, du signalement à la non-régressionFLOT D'UNE ANOMALIEreproduiretest qui échoueavant tout correctifcorrigertest au vertnoteau dépôt
Le test précède le correctif. Sans lui, rien ne prouve que la cause a été traitée.
Procédures nommées et connecteurs de lecture branchés sur l'assistant.PROCÉDURES OUTILLÉES, RAPPELÉES PAR LEUR NOM/dev-featurele flot entier, du scope au push/product-management:write-speccompte rendu fonctionnel/design:design-critiquesur la maquette/engineering:testing-strategyavant d’écrire les tests/verifyà chaque recette locale/engineering:code-reviewaprès la recette, avant le push/simplifyaprès la code review/security-reviewauth, droits, nouvel endpoint/engineering:deploy-checklistà la mise en production/weekly-maintenancepasse hebdomadaireOUTILS BRANCHÉS SUR L'ASSISTANT (MCP ET LIGNE DE COMMANDE)assistantGitHubPR et CIGoogle CloudjournauxSonarCloudqualitéSentryerreursPostHogusageMongoDBbase devResende-mails
Les 10 procédures sont rappelées par leur nom : l'assistant recharge la marche à suivre au lieu que je la redécrive.
Les outils branchés : 7 connecteurs donnent à l'assistant une lecture directe de l'état réel, au lieu de ce que je lui en raconte. Ils servent à constater : aucun ne décide, aucun ne court-circuite les quatre points de validation ni la chaîne de contrôle.
Des règles exécutablesUne convention non outillée dépend de la vigilance ; un script, non.
Une règle qu'aucun script ne vérifie finit par être contournée, quel que soit l'auteur de la ligne.
Les pièges sont versionnésLes pièges sont écrits dans des fichiers versionnés que l'assistant relit à chaque session.
Chaque piège diagnostiqué une fois est consigné et rechargé. Le même problème ne se diagnostique pas deux fois.
Quatre points de validationQuatre moments où la machine s'arrête et attend une décision humaine avant de continuer.
Cadrage, compte rendu fonctionnel, maquette, recette locale. Rien ne continue sans mon accord.
La chaîne, pas la confianceLe même passage obligé s'applique à une ligne écrite à la main et à une ligne générée.
19 checks de CI séparent une ligne de la production, quelle que soit son origine.
14 / Trajectoire
6 mois, 9 paliers livrés
1250 commits depuis février 2026. Chaque palier est parti en production avant que le suivant ne s'ouvre. Les 4 derniers repères sont la suite prévue, pas du travail fait.
Février 2026
MVPL'objectif était de valider le parcours, pas la technique : la production existe dès les premiers jours.
Le parcours entier d'une soirée, du lien de partage au film tiré. La chaîne de déploiement existe avant la première fonctionnalité.
Créer une soirée
Proposer et voter
Tirage à la roue
Lien de partage
Déploiement automatisé
Mars 2026
Migration du serveurLa bascule est faite à contrat identique : si le front continue de fonctionner sans modification, la migration est réussie.
Node et Express remplacés par ASP.NET Core sans changer une route ni un champ JSON. Le front sert de témoin et ne bouge pas.
ASP.NET Core
Routes identiques
Architecture en couches
Image Docker
Mai 2026
V1Premier palier où une machine, et non une relecture, décide si le code atteint la production.
Le démonstrateur devient un service : comptes, réglages de soirée, deux langues, et une chaîne qui peut refuser un push.
Comptes et mot de passe oublié
Réglages de soirée par l'hôte
Marqueur déjà vu
Disponibilité streaming
Thème sombre
Français et anglais
Analyse statique bloquante
Mai 2026
V1.1Palier de confort : peu de structure nouvelle, beaucoup de valeur perçue côté utilisateur.
Fiches films enrichies, ouverture directe vers les plateformes, et les premières notifications hors de l'onglet.
Note, durée, bande-annonce
Liens streaming directs
Soirées passées
Couleur personnalisable
Notifications push VAPID
Application installable
Juin 2026
V1.2Le bandeau de consentement est posé avant l'analytics, jamais l'inverse.
La dimension sociale arrive, et avec elle le consentement : aucun script tiers ne se charge avant un choix explicite.
Profils publics
Abonnements entre membres
Notifications in-app
Statistiques de profil
Consentement granulaire
Analytics produit
Juin à juillet 2026
V1.3Palier réglementaire : le produit devient ouvrable à des tiers sans dette juridique.
La roue devient une vraie roue, le compte devient exportable et supprimable, et l'accessibilité est auditée écran par écran.
Roue animée
Export et suppression RGPD
Recherche avancée
Location et achat
Refonte de la page soirée
Accessibilité auditée
Capture des erreurs
Août 2026
V1.4Le plus gros palier en volume : cinq surfaces produit nouvelles et trois intégrations externes.
Le produit s'ouvre sur l'extérieur : identités déléguées, bibliothèque personnelle et données importées d'un service tiers.
Watchlist personnelle
Synchronisation Letterboxd
Connexion Google et GitHub
Choix manuel du gagnant
Dons Ko-fi
Série de soirées
Proposer une idée
Septembre 2026
V1.5Les règles d'interface passent du document au script : une valeur en dur fait échouer la construction.
Les valeurs d'interface deviennent un jeu fermé vérifié par script, et la racine du site redevient une page publique.
Jetons de design fermés
Contrôle automatique du CSS
Primitives partagées
Accueil public refondu
Ce dossier technique
Septembre 2026
V1.6Le mode tournoi a quitté ce palier pour le backlog : son coût dépassait à lui seul celui des cinq autres items réunis.
La boucle sociale ouverte en V1.2 se referme et la soirée devient un rituel : elle se répète, se rejoue depuis un modèle et peut couronner plusieurs films.
Recherche d'utilisateurs
Soirées récurrentes
Modèles de soirée
Plusieurs gagnants
Plage de votes réglable
Watchlist d'un autre compte
Ensuiteà venir
V1.7La note et le partage sont les deux maillons du parcours où l'application ne fait encore rien, et le partage est le seul qui ramène du monde de l'extérieur.
Fermer la boucle après la soirée : chacun note le film vu, le recap se partage et ramène de nouveaux hôtes, et le profil se personnalise.
Note d'un film vu
Relance du lendemain
Page recap publique
Partage en story
Top 3 films préférés
Photo de profil
Pioche dans la watchlist
Plus tardà venir
V1.8Le temps réel est le dernier chantier de plateforme encore ouvert sur la trajectoire produit.
Remplacer le rafraîchissement périodique par une vraie connexion temps réel, et armer l'hôte des outils qui lui manquent.
Synchronisation temps réel
Présence sur la page soirée
Co-hôte
Thème imposé
Avertissements de contenu
Double authentification
Centre d'aide
Plus tard encoreà venir
V1.9L'import Letterboxd remonte cette fois les films vus et leur note, là où la V1.4 ne synchronisait que la liste à voir.
La bibliothèque personnelle et le confort au quotidien : les films déjà vus repris de Letterboxd, la palette de commandes, la lecture hors-ligne.
Films vus importés de Letterboxd
Palette de commandes
Consultation hors-ligne
Écart watchlist Letterboxd
Entre la V1.9 et la V2
Des versions qui ne sont pas encore cadrées. Leur nombre et leur contenu dépendront des retours d’usage.
Sans date annoncéeà venir
V2Aucune date annoncée : le périmètre dépendra de ce que la V1.9 aura laissé derrière elle.
Une application mobile native, pleinement intégrée à la plateforme. Le prototype de cours a été archivé plutôt que rafistolé.
Application native
Notifications système
Parcours complet hors navigateur
Les chantiers techniques ouverts
Ce qui n’est pas fait, et qui est nommé plutôt que passé sous silence.
Infrastructure en codeLe registre, les secrets, Cloud Run et Firebase Hosting sont décrits en Terraform, avec un état distant ; les identités, le pipeline, les alertes et la recette restent à écrire.
Environnement de recetteIl n’existe qu’une production. Un environnement calqué dessus permettrait de rejouer un déploiement avant qu’il ne compte.
Droits au plus justeLe compte de service de déploiement a plus de droits que nécessaire ; le découper par usage est le pas suivant.
Cache partagé entre instancesLe cache des fiches TMDB vit dans la mémoire de chaque instance : deux instances refont le même appel, et un redémarrage repart à froid. Un cache commun corrigerait les deux.
Adrien Morand. Projet personnel conçu, développé et exploité seul, de la première ligne à la mise en production. Les chiffres sont mesurés dans le dépôt au build, pas estimés.
Vos préférences de confidentialité
Nous utilisons des cookies pour le bon fonctionnement du site. Vous pouvez choisir les catégories que vous autorisez.