pablo.mourato
PROJETS / DÉBRIEFÉTUDE DE CAS · 02 / 05

Débrief.

Des rétrospectives d'équipe en temps réel : chacun écrit ses cartes sans être influencé, on révèle tout d'un coup, on vote à l'aveugle et on repart avec des actions. Conçu, développé, testé et déployé seul, en une semaine.

RÔLE
Conception, back-end, front-end, déploiement
CONTEXTE
Second projet full-stack du portfolio, complémentaire de Créno
ANNÉE · DURÉE
2026 · 1 semaine
STACK
NestJS 11, Socket.IO, Drizzle, PostgreSQL 16, React 19, TypeScript
Tableau de rétro Débrief pendant la discussion : cartes triées par votes et actions de l'équipe
01/ — CONTEXTE

Le besoin.

Une rétrospective réussie repose sur la franchise de chacun. Autour d'une table ou d'un tableau partagé classique, les premiers à parler orientent tous les autres, et les votes à main levée suivent la majorité.

Débrief reprend le déroulé d'une rétro agile et le fait respecter par le logiciel : l'animateur fait avancer l'équipe étape par étape, et chacun ne voit que ce que l'étape permet de voir. Pour mon portfolio, c'était l'occasion de montrer autre chose que Créno : du temps réel, et une stack TypeScript de bout en bout.

OBJECTIF 1Écrire sans voir les cartes des autres, pour éviter l’effet de groupe
OBJECTIF 2Voir toute l’équipe réagir en direct, sans recharger la page
OBJECTIF 3Rejoindre en dix secondes : un code, un prénom, aucun compte
02/ — ARCHITECTURE

Agir en REST,
recevoir en direct.

Toutes les modifications passent par une API REST NestJS : validation, codes d'erreur explicites, faciles à tester. Après chaque modification, le serveur pousse par WebSocket (Socket.IO) le nouvel état du tableau à chaque participant connecté, et le front remplace simplement son cache TanStack Query.

Front ReactReact 19 · TypeScript · TanStack Query · socket.io-client · Vercel
↓ REST + JWT↑ WebSocket : vue personnelle
API NestJSSocket.IO · Drizzle ORM · class-validator · Render
↓ SQL paramétré↑ verrous, index uniques
PostgreSQL 16Rétros, participants, cartes, votes, actions · Neon
29 TESTS D'INTÉGRATION CONTRE UN VRAI POSTGRESQL (TESTCONTAINERS)
03/ — FONCTIONNALITÉS

Cinq étapes,
un seul tableau.

L'animateur pilote, les participants suivent : tout se passe sur le même tableau, qui change selon l'étape.

Étape d’écriture : les cartes des autres participants restent retournées
01

Écriture à l’aveugle

Chacun écrit dans trois colonnes. Les cartes des autres restent retournées ; on voit seulement qui avance.

Étape de révélation : toutes les cartes sont visibles
02

Révélation

L'animateur révèle tout d'un coup : les cartes se retournent chez tout le monde au même instant.

Étape de vote : cartes votées mises en avant, totaux masqués
03

Vote à l’aveugle

Chacun dispose de quelques votes. Les totaux restent secrets jusqu'à la discussion, pour ne pas suivre la foule.

Discussion : cartes triées par votes et panneau des actions
04

Discussion et actions

Les cartes arrivent triées par votes. L'équipe note ses actions, avec un responsable, liées à la carte d'origine.

Fin de rétro : synthèse et export du compte rendu
05

Compte rendu

Une synthèse et un export Markdown, prêt à coller dans Notion, Confluence ou GitHub.

Trois écrans de Débrief sur téléphone
06

Depuis son téléphone

Un code de 6 caractères ou un lien, un prénom : chacun rejoint le tableau en quelques secondes, sans compte.

← FAITES DÉFILER →
04/ — DÉFIS TECHNIQUES

Ce qui a
résisté.

DÉFI 01

Des cartes vraiment secrètes

LE PROBLÈME

Masquer les cartes des autres côté front aurait suffi à l'écran, mais leur contenu serait resté lisible dans l'onglet Réseau du navigateur. Même chose pour les totaux de votes.

MA SOLUTION

Le serveur n'envoie jamais un état commun à la salle : pour chaque socket, une fonction pure construit la vue de ce participant, sans le contenu des cartes des autres pendant l'écriture ni les totaux pendant le vote. L'auteur d'une carte n'est jamais transmis.

board.gateway.ts et board-view.ts — extraits
// Pour CHAQUE participant connecté : sa propre vue du tableau
for (const socket of sockets) {
  const view = buildView(data, socket.data.participantId, online)
  socket.emit('board:state', view)
}

// Dans buildView() : ce qui ne doit pas quitter le serveur
content: hideOthers && !mine ? null : card.content,
votes: showTotals ? (votesByCard.get(card.id) ?? 0) : null,
DÉFI 02

Un plafond de votes qui tient sous la charge

LE PROBLÈME

Chacun dispose de 3 votes. Si un participant clique très vite, ou si plusieurs requêtes arrivent en même temps, chacune peut compter les votes déjà posés avant que les autres n'enregistrent le leur : toutes passent.

MA SOLUTION

Le vote verrouille la ligne du participant (SELECT … FOR UPDATE) le temps de compter ses votes et d'insérer le nouveau : les requêtes simultanées passent l'une après l'autre. Un test envoie 8 votes en parallèle avec un plafond de 3 : exactement 3 sont acceptés.

boards.service.ts — extrait simplifié
await db.transaction(async (tx) => {
  // Verrou sur le participant : ses votes simultanés passent un par un
  await tx.execute(sql`select 1 from participants where id = ${me} for update`)
  const [{ n }] = await tx.select({ n: count() }).from(votes).where(mesVotes)
  if (n >= board.votesPerPerson) throw new ConflictException('Plus de vote disponible.')
  await tx.insert(votes).values({ cardId, participantId: me, boardId })
})
DÉFI 03

Revenir sans créer de doublon

LE PROBLÈME

Les participants n'ont pas de compte. En rouvrant le lien, en changeant d'appareil ou de compte sur le même navigateur, ils reprenaient une nouvelle place et apparaissaient en double.

MA SOLUTION

Le navigateur garde deux places distinctes par rétro, invité et animateur, et chaque invité dispose d'un lien personnel pour changer d'appareil. En base, un index unique sur (rétro, prénom en minuscules) garantit qu'il n'y a jamais deux « Léa ».

0001_unique_participant_name.sql — extrait
-- Un prénom par personne dans une rétro, casse ignorée (« Léa » = « léa »)
CREATE UNIQUE INDEX "ux_participants_board_name"
  ON "participants" ("board_id", lower("name"));
05/ — RÉSULTATS

En ligne.

29tests d'intégration, contre un vrai PostgreSQL
3 / 8votes acceptés quand 8 arrivent ensemble avec un plafond de 3
< 1 sde démarrage pour l’API Node, contre 63 s pour Spring sur la même offre

L'application est en ligne : l'animateur crée une rétro, les participants la rejoignent avec un code depuis leur téléphone. Le code est public, avec un README qui détaille l'architecture, la sécurité et les tests.

CE QUE J'EN RETIENS

La gestion du temps réel ne se limite pas à la diffusion d’événements : elle implique également de contrôler précisément les données accessibles à chaque utilisateur. Même sans compte, un invité doit disposer d’une identité fiable et sécurisée, garantissant la cohérence des interactions au sein de l’application.