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 roundtableMatchingStartedroundtableMatchingProgress (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.