Appearance
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 :
- En-tête
Authorizationau formatBearer <token>. handshake.auth.token— l'optionauthdu client Socket.IO, pratique quand un en-tête personnalisé n'est pas envoyable par la plateforme cliente.- 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é :
code | Cas |
|---|---|
NO_TOKEN | Aucun jeton trouvé dans les 3 emplacements ci-dessus |
TOKEN_EXPIRED | JWT expiré |
INVALID_TOKEN | JWT malformé/signature invalide |
TOKEN_REVOKED | Jeton présent dans la blacklist |
USER_NOT_FOUND | Le sub du JWT ne correspond à aucun utilisateur |
AUTHENTICATION_FAILED | Toute 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
joinedEventStaffavec{ eventId }. - Échec : événement
erroravecEVENT_NOT_FOUND(événement inexistant) ouEVENT_ACCESS_DENIED(pas autorisé). - Pour quitter : message
leaveEventStaffavec{ eventId }, réponseleftEventStaff.
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 :
joinedEventParticipantsavec{ eventId }. - Échec :
erroravecEVENT_ACCESS_DENIED— « Not registered to this event ». - Pour quitter : message
leaveEventParticipantsavec{ eventId }, réponseleftEventParticipants.
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.
| Canal | Audience | Payload (hors timestamp) |
|---|---|---|
roundtableMatchingStarted | staff | { eventId, sessionId, mode: 'INITIAL' | 'RERUN' } |
roundtableMatchingProgress | staff | { eventId, sessionId, stage, done, total, diagnostics? } — stage ∈ 'starting'|'cache'|'embeddings'|'llm'|'optimizer'|'persisting' |
roundtableMatchingCompleted | both | { eventId, sessionId } |
roundtableMatchingFailed | staff | { eventId, sessionId, error } |
roundtableSessionStarted | both | { eventId, sessionId } |
roundtableSessionPaused | both | { eventId, sessionId } |
roundtableSessionResumed | both | { eventId, sessionId } |
roundtableSessionCompleted | both | { eventId } |
roundtableSessionReset | both | { eventId, sessionId } |
roundtableRotationStarted | both | { eventId, rotationNumber } |
roundtableRotationCompleted | both | { eventId, rotationNumber } |
roundtableRotationExtended | both | { eventId, sessionId, minutes } |
roundtableRotationTimerReset | both | { eventId, sessionId } |
roundtableRotationReset | both (room) | { eventId, sessionId, rotationId, rotationNumber } |
roundtableAssignmentUpdated | utilisateurs 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 |
roundtableAssignmentsRecalculated | soit staff (room event:{id}:staff) soit utilisateurs affectés (envoi direct), selon le contexte — voir note ci-dessous | Variable 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 :
- 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,roundtableAssignmentUpdatedavec{ eventId }. - 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/roundtableMatchingFailedplutô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). UnsetIntervalclient naïf dérive de l'horloge serveur ; recalculez plutôt le temps restant à partir de ces champs à chaque réception deroundtableRotationStarted,roundtableRotationExtendedouroundtableRotationTimerReset, 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 / message | Sens | Payload |
|---|---|---|
joinChat (émis par le client) → joinedChat | Rejoindre 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 } |
messageCreated | Nouveau message dans un chat | { message } |
chatRead | Accusé de lecture | { chatId, userId, lastReadAt } |
staffAnnouncement (émis par le client, room event:{id}:staff uniquement) → rediffusé à la même room | Annonce 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.