DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Server-Sent Events with Claude Code: Real-Time Push Without WebSocket Complexity

Notifications, progress bars, live feeds — these don't need WebSocket's bidirectional complexity. Server-Sent Events give you server-to-client streaming with automatic reconnection built in. Claude Code generates the full implementation.


CLAUDE.md for SSE Standards

## Server-Sent Events (SSE) Rules

### When SSE is the right choice
- Server → client one-way streaming
- Notifications, progress updates, live feeds, dashboard updates
- No client-to-server data needed

### Use WebSocket instead when
- Bidirectional communication (chat, games, collaboration)
- Binary data transfer

### Implementation rules
- Headers: Content-Type: text/event-stream, Cache-Control: no-cache
- X-Accel-Buffering: no (disable Nginx buffering)
- Auth: query parameter token (EventSource can't set Authorization header)
- Reconnection: use Last-Event-ID to resume from where client left off
- Heartbeat: send comment every 30 seconds to keep connection alive
- Max connections: 5 per user

### Security
- SSE endpoints require authentication (same as any API)
- Set CORS headers appropriately
- Validate token in query parameter before sending any events
Enter fullscreen mode Exit fullscreen mode

Generating the SSE Server

Generate an SSE endpoint with Express + TypeScript.

Requirements:
- Endpoint: GET /api/events
- JWT auth via query parameter: ?token=xxx
- Event types: notification, update, heartbeat
- Heartbeat: every 30 seconds
- Connection management: Map by userId
- Auto-cleanup on disconnect

Generate:
- src/sse/sseManager.ts (connection manager)
- src/routes/events.ts (SSE endpoint)
Enter fullscreen mode Exit fullscreen mode

Generated SSE Manager

// src/sse/sseManager.ts
import { Response } from 'express';

interface SSEClient {
  userId: string;
  res: Response;
  lastEventId?: string;
}

interface SSEEvent {
  type: string;
  data: unknown;
  id?: string;
}

class SSEManager {
  private clients = new Map<string, Set<SSEClient>>();

  constructor() {
    // Heartbeat every 30 seconds to keep connections alive
    setInterval(() => {
      this.broadcastComment('heartbeat');
    }, 30_000);
  }

  addClient(userId: string, client: SSEClient): boolean {
    if (!this.clients.has(userId)) {
      this.clients.set(userId, new Set());
    }

    const userClients = this.clients.get(userId)!;

    if (userClients.size >= 5) {
      return false; // Max connections exceeded
    }

    userClients.add(client);

    client.res.on('close', () => {
      userClients.delete(client);
      if (userClients.size === 0) {
        this.clients.delete(userId);
      }
    });

    return true;
  }

  sendToUser(userId: string, event: SSEEvent): void {
    const userClients = this.clients.get(userId);
    if (!userClients) return;

    for (const client of userClients) {
      this.writeEvent(client.res, event);
    }
  }

  broadcast(event: SSEEvent): void {
    for (const clients of this.clients.values()) {
      for (const client of clients) {
        this.writeEvent(client.res, event);
      }
    }
  }

  private broadcastComment(comment: string): void {
    for (const clients of this.clients.values()) {
      for (const client of clients) {
        client.res.write(`: ${comment}

`);
      }
    }
  }

  private writeEvent(res: Response, event: SSEEvent): void {
    if (event.id) res.write(`id: ${event.id}
`);
    res.write(`event: ${event.type}
`);
    res.write(`data: ${JSON.stringify(event.data)}

`);
  }
}

export const sseManager = new SSEManager();
Enter fullscreen mode Exit fullscreen mode
// src/routes/events.ts
eventsRouter.get('/events', async (req, res) => {
  // Query param token — EventSource can't set Authorization header
  const token = req.query.token as string;
  const user = await verifyToken(token);
  if (!user) return res.status(401).end();

  // SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no'); // Nginx: disable buffering
  res.flushHeaders();

  const added = sseManager.addClient(user.id, {
    userId: user.id,
    res,
    lastEventId: req.headers['last-event-id'] as string,
  });

  if (!added) {
    res.write('event: error
data: {"message":"max connections exceeded"}

');
    return res.end();
  }

  // Confirm connection
  res.write(`event: connected
data: {"userId":"${user.id}"}

`);
});
Enter fullscreen mode Exit fullscreen mode

Sending Events from Services

// From any service — send notification to specific user
import { sseManager } from '../sse/sseManager';

// When order is placed
async function onOrderPlaced(order: Order) {
  sseManager.sendToUser(order.userId, {
    type: 'notification',
    data: {
      title: 'Order placed',
      message: `Order #${order.id} confirmed`,
      orderId: order.id,
    },
    id: order.id, // For Last-Event-ID reconnection
  });
}
Enter fullscreen mode Exit fullscreen mode

React Client Hook

Generate a React hook for SSE connection.

Requirements:
- Custom hook: useSSE(url, options)
- Per-event-type callbacks
- Auto-reconnect using EventSource native behavior (Last-Event-ID)
- Show error state in UI on connection failure
- EventSource.close() on component unmount
Enter fullscreen mode Exit fullscreen mode
// src/hooks/useSSE.ts
import { useEffect, useRef } from 'react';

interface SSEOptions {
  onNotification?: (data: unknown) => void;
  onUpdate?: (data: unknown) => void;
  onError?: (error: Event) => void;
}

export function useSSE(url: string, options: SSEOptions) {
  const eventSourceRef = useRef<EventSource | null>(null);

  useEffect(() => {
    const es = new EventSource(url);

    es.addEventListener('notification', (e: MessageEvent) => {
      options.onNotification?.(JSON.parse(e.data));
    });

    es.addEventListener('update', (e: MessageEvent) => {
      options.onUpdate?.(JSON.parse(e.data));
    });

    es.onerror = (e) => {
      options.onError?.(e);
      // EventSource reconnects automatically with Last-Event-ID
    };

    eventSourceRef.current = es;

    return () => {
      es.close();
      eventSourceRef.current = null;
    };
  }, [url]);
}
Enter fullscreen mode Exit fullscreen mode

Summary

Implement SSE with Claude Code:

  1. CLAUDE.md — Define use cases, header requirements, auth approach
  2. SSEManager — Map-based connection management per userId
  3. Heartbeat — 30-second comments to keep connections alive
  4. Client hook — Auto-reconnect with Last-Event-ID built into EventSource

Security Pack (¥1,480) includes /security-check for real-time endpoint security review — auth gaps, missing CORS, max connection limits.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Claude Code engineer focused on real-time web patterns.

Top comments (0)