Skip to content

WebSocket ​

Le temps réel (notifications, chat, roundtables) ne passe pas par des subscriptions GraphQL : c'est un canal Socket.IO séparé, avec son propre handshake d'authentification et son propre système de « rooms ». Cette page décrit le contrat observable côté client : comment se connecter, quelles rooms rejoindre, et quels événements écouter.

Transport ​

Le serveur expose une passerelle Socket.IO acceptant les transports websocket et polling. Ce n'est ni GraphQL ni du SSE : utilisez un client Socket.IO standard (socket.io-client côté web/mobile), pas un client GraphQL avec un lien WebSocket.

Authentification au handshake ​

L'authentification se fait à la connexion, avant que le socket ne puisse rejoindre la moindre room. Le serveur cherche le JWT dans cet ordre :

  1. En-tête Authorization au format Bearer <token>.
  2. handshake.auth.token — l'option auth du client Socket.IO, pratique quand un en-tête personnalisé n'est pas envoyable par la plateforme cliente.
  3. Cookie accessToken, en dernier recours.

C'est le même JWT que celui utilisé pour l'API GraphQL — pas de jeton dédié au WebSocket.

Si l'authentification échoue, le serveur émet un événement error avec un code puis ferme la connexion (socket.disconnect()) — le client n'est jamais laissé connecté sans identité :

codeCas
NO_TOKENAucun jeton trouvé dans les 3 emplacements ci-dessus
TOKEN_EXPIREDJWT expiré
INVALID_TOKENJWT malformé/signature invalide
TOKEN_REVOKEDJeton présent dans la blacklist
USER_NOT_FOUNDLe sub du JWT ne correspond à aucun utilisateur
AUTHENTICATION_FAILEDToute autre erreur de vérification

Ces codes d'erreur socket sont également réutilisés pour les erreurs applicatives plus bas (accès refusé à une room, validation, etc.), pas seulement pour l'authentification.

Une fois authentifié, le socket rejoint automatiquement sa room privée user:{userId} — voir ci-dessous.

Rooms ​

Le serveur route les événements via des « rooms » Socket.IO. Un client ne reçoit que les événements des rooms qu'il a rejointes.

user:{id} — notifications et chat personnels ​

Rejointe automatiquement à la connexion, pour l'utilisateur authentifié uniquement. Reçoit notamment :

  • notificationCreated — { notification }
  • chatCreated / chatUpdated — { chat }, émis à chaque participant du chat

event:{eventId}:staff — dashboard staff d'un événement ​

Rejointe explicitement en envoyant le message joinEventStaff avec { eventId }. L'autorisation est vérifiée côté serveur avant que le join n'ait lieu : organisateur de l'événement, ADMIN/SUPER_ADMIN, ou rôle business club actif avec l'une des permissions PARTICIPANT_CHECK_IN / ROUNDTABLE_MANAGE / ROUNDTABLE_VIEW_DASHBOARD, ou une affectation de staff explicite sur cet événement.

  • Succès : le serveur émet joinedEventStaff avec { eventId }.
  • Échec : événement error avec EVENT_NOT_FOUND (événement inexistant) ou EVENT_ACCESS_DENIED (pas autorisé).
  • Pour quitter : message leaveEventStaff avec { eventId }, réponse leftEventStaff.

event:{eventId}:participants — vue participant d'un événement ​

Rejointe via le message joinEventParticipants avec { eventId }. Autorisé si l'appelant est ADMIN/SUPER_ADMIN, ou s'il a une inscription à cet événement.

  • Succès : joinedEventParticipants avec { eventId }.
  • Échec : error avec EVENT_ACCESS_DENIED — « Not registered to this event ».
  • Pour quitter : message leaveEventParticipants avec { eventId }, réponse leftEventParticipants.

Ne rejoignez pas ces rooms de manière opportuniste : elles portent des données potentiellement sensibles (affectations de table, messages staff). Rejoignez uniquement la room correspondant au rôle réel de l'utilisateur pour cet événement précis, et quittez-la quand l'utilisateur navigue ailleurs.

Canaux roundtable (speed-networking) ​

Les rotations et le matching IA d'un événement (« tables rondes ») sont pilotés côté serveur et diffusés en temps réel sur les rooms event:{eventId}:staff / event:{eventId}:participants ci-dessus. Chaque payload est enrichi côté serveur d'un champ timestamp ISO 8601 avant émission — il n'a pas besoin d'être fourni par l'appelant et est garanti présent sur tous les canaux listés ci-dessous.

CanalAudiencePayload (hors timestamp)
roundtableMatchingStartedstaff{ eventId, sessionId, mode: 'INITIAL' | 'RERUN' }
roundtableMatchingProgressstaff{ eventId, sessionId, stage, done, total, diagnostics? } — stage ∈ 'starting'|'cache'|'embeddings'|'llm'|'optimizer'|'persisting'
roundtableMatchingCompletedboth{ eventId, sessionId }
roundtableMatchingFailedstaff{ eventId, sessionId, error }
roundtableSessionStartedboth{ eventId, sessionId }
roundtableSessionPausedboth{ eventId, sessionId }
roundtableSessionResumedboth{ eventId, sessionId }
roundtableSessionCompletedboth{ eventId }
roundtableSessionResetboth{ eventId, sessionId }
roundtableRotationStartedboth{ eventId, rotationNumber }
roundtableRotationCompletedboth{ eventId, rotationNumber }
roundtableRotationExtendedboth{ eventId, sessionId, minutes }
roundtableRotationTimerResetboth{ eventId, sessionId }
roundtableRotationResetboth (room){ eventId, sessionId, rotationId, rotationNumber }
roundtableAssignmentUpdatedutilisateurs affectés (envoi direct sur user:{id}, pas de room event:*){ eventId } — signal minimal : ne contient pas la nouvelle table, sert de déclencheur pour refetch
roundtableAssignmentsRecalculatedsoit staff (room event:{id}:staff) soit utilisateurs affectés (envoi direct), selon le contexte — voir note ci-dessousVariable selon le contexte, voir note

Le canal roundtableAssignmentsRecalculated a deux formes distinctes

Ce nom de canal est réutilisé dans deux contextes différents avec des payloads différents — ne supposez pas une forme unique :

  1. Recalcul global : diffusé à la room event:{eventId}:staff, payload { eventId, affectedCount } (juste un compteur, pour rafraîchir un dashboard staff). Dans ce même événement, chaque utilisateur affecté reçoit en plus, individuellement, roundtableAssignmentUpdated avec { eventId }.
  2. Reset d'une rotation ciblée : envoyé directement (pas via la room) à chaque utilisateur affecté par le reset, payload { eventId, sessionId, rotationId, rotationNumber }.

Dans les deux cas, le payload ne porte pas la nouvelle affectation de table — c'est un signal « quelque chose a changé pour toi, refais la requête ».

Le client écoute, il ne recalcule pas ​

Le matching et la gestion des rotations sont asynchrones côté serveur (appels OpenAI, optimisation batch). Le client doit :

  • Écouter roundtableMatchingStarted → roundtableMatchingProgress (répété) → roundtableMatchingCompleted/roundtableMatchingFailed plutôt que de sonder (poll) une query en boucle.
  • Ne pas maintenir un minuteur de rotation indépendant. Le temps restant d'une rotation est dérivé côté serveur de l'heure de démarrage et de la durée configurée — champs exposés par les queries roundtable (par exemple rotationStartedAt, rotationExtraSeconds). Un setInterval client naïf dérive de l'horloge serveur ; recalculez plutôt le temps restant à partir de ces champs à chaque réception de roundtableRotationStarted, roundtableRotationExtended ou roundtableRotationTimerReset, en refaisant la query pour obtenir les valeurs à jour.

Autres canaux notables ​

Ces canaux existent sur la même passerelle mais en dehors du périmètre roundtable ci-dessus — utiles pour un client de chat/notifications complet :

Canal / messageSensPayload
joinChat (émis par le client) → joinedChatRejoindre la room chat:{chatId}, vérifie que l'appelant est participant du chat{ chatId }
typing (émis par le client) → userTyping (broadcast aux autres participants)Indicateur de frappe{ userId, chatId, isTyping, user }
messageCreatedNouveau message dans un chat{ message }
chatReadAccusé de lecture{ chatId, userId, lastReadAt }
staffAnnouncement (émis par le client, room event:{id}:staff uniquement) → rediffusé à la même roomAnnonce staff-à-staff ; message tronqué à 500 caractères côté serveur ; l'autorisation staff est revérifiée à chaque envoi, pas seulement au join{ message, senderName, senderId, timestamp }

Erreurs applicatives ​

Au-delà des erreurs d'authentification, toute violation d'autorisation ou de validation sur un message émis par le client renvoie un événement error avec un des codes d'erreur socket (ex. EVENT_ACCESS_DENIED, CHAT_NOT_FOUND, VALIDATION_ERROR, INTERNAL_ERROR). Le socket n'est pas nécessairement déconnecté dans ces cas (contrairement aux échecs d'authentification) — le client reste connecté et peut retenter avec des paramètres corrects.