# Dev Websocket Designer

> Conception de systèmes temps réel avec WebSocket — SignalR, Socket.io, scaling horizontal et stratégies de reconnexion. Se déclenche avec "WebSocket", "temps réel", "SignalR", "Socket.io", "real-time", "push notifications". Also triggers on "WebSocket design", "real-time updates", "SignalR or Socket.io scaling".

- Skill: `khalilbenaz/dev-websocket-designer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add khalilbenaz/dev-websocket-designer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/khalilbenaz/dev-websocket-designer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: khalilbenaz (https://skillmd.com/u/khalilbenaz)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/khalilbenaz/dev-websocket-designer

---


# WebSocket Designer

## 1. Choisir le bon protocole temps réel

| Besoin | Solution recommandée | Raison |
|---|---|---|
| Bidirectionnel, faible latence | WebSocket natif / SignalR / Socket.io | Full-duplex sur une seule connexion TCP |
| Serveur → client uniquement | SSE (Server-Sent Events) | Plus simple, traversée proxy meilleure |
| Polling toléré, contraintes infra | Long polling | Fonctionne partout sans port spécial |
| < 1 000 connexions simultanées | WebSocket natif | Pas besoin de lib externe |
| Stack .NET | SignalR | Négociation automatique + fallback intégré |
| Stack Node.js | Socket.io | Rooms, namespaces, reconnexion built-in |

**Règle décisive** : si un proxy ou load balancer ne supporte pas l'upgrade HTTP→WS, préfère SSE ou assure-toi que le backplane est configuré avant de commencer.

---

## 2. Concevoir le protocole de messages

Définis un contrat JSON structuré avant d'écrire une ligne de code.

```jsonc
// Enveloppe standard
{
  "type": "CHAT_MESSAGE",       // action discriminante
  "channel": "room:42",         // scope / destinataire
  "payload": { "text": "..." }, // données métier
  "correlationId": "uuid-v4",   // pour ack / replay
  "ts": 1719230400000           // timestamp epoch ms
}
```

Types d'événements minimum à définir :
- `SUBSCRIBE` / `UNSUBSCRIBE` — gestion des canaux
- `ACK` — acquittement d'un message critique
- `ERROR` — retour d'erreur structuré `{ code, message }`
- `PING` / `PONG` — heartbeat applicatif

---

## 3. Implémenter le serveur

### SignalR (.NET 8)

```csharp
// Program.cs
builder.Services.AddSignalR(o => {
    o.MaximumReceiveMessageSize = 32 * 1024; // 32 KB max
    o.EnableDetailedErrors = app.Environment.IsDevelopment();
});
app.MapHub<ChatHub>("/hubs/chat");

// ChatHub.cs
[Authorize]
public class ChatHub : Hub
{
    public async Task SendMessage(string channel, string text)
    {
        // Vérifier l'autorisation sur le canal
        if (!await _authService.CanWriteAsync(Context.UserIdentifier, channel))
            throw new HubException("Forbidden");

        await Clients.Group(channel).SendAsync("ReceiveMessage", new {
            From = Context.UserIdentifier,
            Text = text,
            Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
        });
    }

    public override async Task OnConnectedAsync()
    {
        // Rejoindre les canaux de l'utilisateur dès la connexion
        var channels = await _channelService.GetUserChannelsAsync(Context.UserIdentifier);
        foreach (var ch in channels)
            await Groups.AddToGroupAsync(Context.ConnectionId, ch);

        await base.OnConnectedAsync();
    }
}
```

### Socket.io (Node.js / TypeScript)

```typescript
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";

const io = new Server(httpServer, {
  cors: { origin: process.env.ALLOWED_ORIGIN },
  maxHttpBufferSize: 32 * 1024, // 32 KB
  pingTimeout: 20_000,
  pingInterval: 10_000,
});

io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  const user = await verifyJwt(token); // lance une erreur si invalide
  socket.data.user = user;
  next();
});

io.on("connection", async (socket) => {
  const { userId } = socket.data.user;
  const channels = await getChannels(userId);
  socket.join(channels);

  socket.on("CHAT_MESSAGE", async ({ channel, text }) => {
    if (!channels.includes(channel)) return socket.emit("ERROR", { code: 403 });
    io.to(channel).emit("CHAT_MESSAGE", { from: userId, text, ts: Date.now() });
  });
});
```

---

## 4. Implémenter le client avec reconnexion robuste

```typescript
// Reconnexion avec exponential backoff + jitter (vanilla WS)
function createWebSocketClient(url: string) {
  let ws: WebSocket;
  let attempt = 0;
  const MAX_DELAY = 30_000;

  function connect() {
    ws = new WebSocket(url);

    ws.onopen = () => {
      attempt = 0;
      // Rejouer les messages en attente
      drainQueue();
    };

    ws.onmessage = (e) => handleMessage(JSON.parse(e.data));

    ws.onclose = (e) => {
      if (e.code === 1000) return; // fermeture propre
      const delay = Math.min(1_000 * 2 ** attempt + Math.random() * 500, MAX_DELAY);
      attempt++;
      setTimeout(connect, delay);
    };
  }

  connect();
  return { send: (msg: object) => ws.send(JSON.stringify(msg)) };
}
```

**SignalR client** : utilise `withAutomaticReconnect([0, 2000, 5000, 10000, 30000])` — les intervalles sont en ms.

---

## 5. Scaler horizontalement

### Redis backplane SignalR

```csharp
builder.Services.AddSignalR()
    .AddStackExchangeRedis("redis:6379,abortConnect=false", o => {
        o.Configuration.ChannelPrefix = RedisChannel.Literal("myapp");
    });
```

### Redis adapter Socket.io

```typescript
import { createClient } from "redis";
import { createAdapter } from "@socket.io/redis-adapter";

const pub = createClient({ url: "redis://redis:6379" });
const sub = pub.duplicate();
await Promise.all([pub.connect(), sub.connect()]);
io.adapter(createAdapter(pub, sub));
```

**Points clés** :
- Active les **sticky sessions** sur le load balancer pendant la phase de handshake HTTP (upgrade WS). Après l'upgrade, la connexion TCP est persistante — les sticky sessions ne sont plus nécessaires si le backplane est en place.
- Si tu utilises Nginx : `proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";`

---

## 6. Sécurité

- **Authentification** : JWT via `Authorization: Bearer` header lors du handshake (query string acceptable mais éviter en prod — visible dans les logs).
- **Autorisation** : vérifier les droits à chaque message, pas seulement à la connexion.
- **Rate limiting** : limiter le nombre de messages par connexion/seconde côté serveur.
- **Taille des messages** : impose un maximum (32 KB recommandé) pour éviter les attaques par saturation mémoire.
- **Validation** : parse et valide le payload à chaque réception (zod, FluentValidation...).

---

## 7. Monitoring

Métriques essentielles à exposer (Prometheus / OpenTelemetry) :

| Métrique | Seuil d'alerte typique |
|---|---|
| `ws_connections_active` | > 80 % de la capacité max |
| `ws_messages_per_second` | pic anormal × 3 |
| `ws_reconnect_rate` | > 5 % des clients/min |
| `ws_message_latency_p99` | > 500 ms |
| `ws_errors_total` | toute augmentation soudaine |

---

## Anti-patterns et pièges

- **Broadcast global sans filtrage** : envoyer un message à tous les clients connectés au lieu d'un groupe/canal = charge inutile et fuite de données.
- **Stocker l'état en mémoire du processus** : si plusieurs instances, les clients sur des pods différents ne voient pas le même état. Utilise Redis ou une base partagée.
- **Oublier le heartbeat** : les proxys et équilibreurs de charge coupent les connexions TCP inactives après 60-90 s par défaut. Implémente un ping/pong applicatif toutes les 30 s.
- **Authentification uniquement au handshake** : un token peut expirer en cours de session. Vérifie périodiquement ou à chaque action sensible.
- **Omettre le fallback** : certains réseaux d'entreprise bloquent les WebSockets. SignalR et Socket.io gèrent le fallback automatiquement — ne le désactive pas.
- **Ignorer la backpressure** : si le client est lent à consommer, le buffer serveur grossit. Limite la file et déconnecte les clients trop lents.
- **Protocole non documenté** : traite le contrat de messages comme une API — versionne-le, documente chaque type d'événement.


## Communication Rules — MANDATORY

- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.

