hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Real-Time APIs with @hectoday/http

Beyond Request-Response

  • Why Real-Time Matters
  • Project Setup

Polling

  • Short Polling
  • Long Polling

Server-Sent Events

  • How SSE Works
  • Building an SSE Endpoint
  • Event Types and IDs
  • SSE in Practice

WebSockets

  • How WebSockets Work
  • Building a WebSocket Server
  • Rooms and Broadcasting
  • Authentication on WebSockets
  • Handling Disconnects and Reconnection

Patterns and Architecture

  • Pub/Sub
  • Presence
  • Scaling Real-Time

Putting It All Together

  • Choosing the Right Approach
  • Capstone: Live Task Board

Capstone: Live Task Board

What we built

A real-time collaborative task board with four layers of communication:

LayerProtocolDirectionPurpose
CRUDRESTRequest-responseCreate, update, delete tasks
Board updatesSSEServer → clientPush task changes to viewers
CollaborationWebSocketBidirectionalPresence, typing indicators
FallbackPollingRequest-responseEnvironments without SSE/WS

The complete architecture

Browser
│
├─ REST (fetch)
│   POST /tasks → create task → publish event
│   PATCH /tasks/:id/move → move task → publish event
│   DELETE /tasks/:id → delete task → publish event
│
├─ SSE (EventSource)
│   GET /boards/:boardId/events
│   ← task_created, task_moved, task_deleted
│   ← heartbeat comments (keep-alive)
│   → automatic reconnection with Last-Event-ID
│
└─ WebSocket (ws://)
    → join, leave, typing, stop_typing
    ← user_joined, user_left, user_typing
    ← presence (viewer list)
    → ping/pong (heartbeat)

Server
│
├─ Event Bus (pub/sub)
│   publish("task_created", { task }) → SSE subscriber, WebSocket subscriber
│
├─ Rooms
│   joinRoom(boardId, ws) → track connections per board
│   broadcastToRoom(boardId, data) → send to all in room
│
├─ Presence
│   addPresence(boardId, ws, userId) → track who is viewing
│   getPresence(boardId) → list of viewers
│
└─ SSE Client Registry
    addClient(boardId) → track SSE connections
    broadcast(boardId, data) → send to all SSE clients

The event flow

When Alice creates a task:

  1. Alice’s browser sends POST /tasks (REST)
  2. Server creates the task in the database
  3. Server calls publish(boardId, "task_created", { task }) (event bus)
  4. SSE subscriber receives the event → pushes to all SSE clients watching this board
  5. WebSocket subscriber receives the event → broadcasts to all WebSocket clients in this board’s room
  6. Bob’s browser receives the event (via SSE or WebSocket) and updates the UI

Alice’s REST response includes the created task (standard 201). Bob receives the task via the real-time channel. Both have the same state.

Project structure

src/
  app.ts                # Hectoday HTTP setup, REST routes
  server.ts             # HTTP + WebSocket server
  db.ts                 # Schema, seed data
  event-bus.ts          # Pub/sub event bus
  sse.ts                # SSE client registry, broadcast, event buffer
  ws-server.ts          # WebSocket connection handler, auth
  rooms.ts              # Room join/leave/broadcast
  presence.ts           # User presence tracking
  long-poll.ts          # Long polling (fallback)
  routes/
    tasks.ts            # CRUD + publish events
    events.ts           # SSE endpoint
    boards.ts           # Board listing

Test the complete system

npm run dev

# === REST ===
curl http://localhost:3000/boards/board-1/tasks

# === SSE — Terminal 1 ===
curl -N http://localhost:3000/boards/board-1/events
# Receives: data: {"type":"connected"}

# === WebSocket — Terminal 2 ===
wscat -c ws://localhost:3000
> {"type":"join","boardId":"board-1"}
# Receives: {"type":"presence","viewers":[...]}

# === Create a task — Terminal 3 ===
curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Live from the capstone!","listId":"list-todo"}'

# Terminal 1 (SSE): data: {"type":"task_created","task":{...}}
# Terminal 2 (WS):  {"type":"task_created","task":{...}}

# === Typing indicator ===
# Terminal 2:
> {"type":"typing","boardId":"board-1"}
# Other WebSocket clients receive: {"type":"user_typing",...}

# === Move a task ===
curl -X PATCH http://localhost:3000/tasks/task-1/move \
  -H "Content-Type: application/json" \
  -d '{"listId":"list-done","position":0}'
# SSE and WS clients receive: {"type":"task_moved","task":{...}}

What you understand now

Real-time communication comes down to a few patterns:

Polling is the simplest but least efficient. Use it as a fallback.

SSE is the right choice for server-to-client push. Automatic reconnection, event types, resumption — built into the browser.

WebSockets are for bidirectional communication. Chat, presence, typing indicators — features where both sides send messages.

Pub/sub decouples event producers from consumers. Add new delivery mechanisms (webhooks, push notifications) without changing existing code.

Rooms scope broadcasts to relevant clients. A user on board-1 does not receive board-2 events.

Presence tracks who is online. Deduplicate by user (multiple tabs), clean up on disconnect, broadcast join/leave events.

Heartbeats detect dead connections. Ping-pong for WebSockets, comment lines for SSE.

Reconnection handles network interruptions. Automatic for SSE, manual with exponential backoff for WebSockets. Resync state after reconnecting.

These patterns transfer to any real-time system: Slack, Discord, Figma, Google Docs. The protocols and libraries change. The patterns are the same.

Challenges

Challenge 1: Add cursor tracking. In a collaborative task editor, show where each user’s cursor is. Send cursor position via WebSocket, broadcast to the room, render colored cursors for each user.

Challenge 2: Add optimistic updates. When Alice creates a task, show it in her UI immediately (before the server responds). If the server rejects it, roll back. This makes the UI feel instant.

Challenge 3: Add offline support. Queue mutations when the user is offline. When the connection is restored, replay the queue. Resolve conflicts with server state.

Challenge 4: Add WebSocket-based CRUD. Instead of REST for mutations, send task operations via WebSocket. The server processes them and broadcasts the result. Compare the tradeoffs with the REST + pub/sub approach.

Why does the task board use REST for mutations instead of sending them via WebSocket?

What is the most important real-time pattern from this course?

← Choosing the Right Approach Back to course →

© 2026 hectoday. All rights reserved.