AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Serve Realtime With Websockets

skill-kennguyen887-agent-foundation-serve-realtime-with-websockets · by kennguyen887

Use when pushing realtime updates to connected clients over WebSockets (a socket.io / WS gateway) — authenticating the socket at the handshake (once, not per message), joining sockets to rooms for targeted broadcast, scaling across instances with a Redis pub/sub adapter (+ sticky sessions), bridging domain events to room emits, and connection-lifecycle hygiene (reconnect, listener cleanup). NestJ…

No reviews yet
0 installs
20 views
0.0% view→install

Install

$ agentstack add skill-kennguyen887-agent-foundation-serve-realtime-with-websockets

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kennguyen887-agent-foundation-serve-realtime-with-websockets)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
24d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Serve Realtime With Websockets? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Serve realtime with WebSockets

Pushing live updates to connected clients (call signalling, notifications, presence, live status) over a WebSocket gateway. Examples NestJS + socket.io, neutral domain. principle → ▸ Example▸ Other stacks. Unlike the rest of the backend (request/RPC/queue), a WebSocket holds a long-lived, stateful connection — which changes how you auth, target, and scale. Cross-service events that feed the broadcasts are integrate-internal-services.

Core principle

Authenticate once at the handshake, target with rooms, and scale with a shared pub/sub adapter. A connection is long-lived and pinned to one instance, so: verify identity when it opens (not per message), broadcast to rooms (never a blind global emit), and put a Redis adapter between instances or a multi-pod deploy only reaches the clients on one pod. Keep business logic out of the gateway — it's a transport.

1. The gateway + connection lifecycle

A gateway declares the server, lifecycle hooks, and message handlers:

@WebSocketGateway({ cors: { origin: corsOrigins } })
export class EventsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server!: Server;

  handleConnection(socket: Socket) {
    if (!socket.handshake.auth.userId) { socket.emit('error', 'unauthenticated'); socket.disconnect(); return; }  // reject unauth
  }
  handleDisconnect(socket: Socket) { socket.removeAllListeners(); }   // cleanup — avoid listener leaks
  @SubscribeMessage('ping') ping(@MessageBody() d: unknown) { this.server.emit('pong', d); }
}

Cap listeners (server.setMaxListeners(n) from config) — a busy gateway otherwise trips the max-listeners warning and leaks.

2. Authenticate at the handshake (once), attach identity, join rooms

Use connection middleware — verify the token from the handshake once, attach the resolved identity to the socket, and join its rooms there. Per-message auth is wasteful and error-prone.

server.use(async (socket, next) => {
  const token = socket.handshake.auth.token ?? socket.handshake.query.token;
  const claims = token && await this.auth.verify(token);     // delegate to your auth/IdP (integrate-identity-providers)
  if (!claims) return next(new Error('unauthorized'));        // reject the connection
  socket.handshake.auth = { userId: claims.sub, tenantId: claims.orgId, role: claims.role };  // trusted identity
  socket.join(`tenant:${claims.orgId}`);                      // §3 rooms
  socket.join('everyone');
  next();
});

3. Rooms for targeted broadcast (not blind global emits)

Join each socket to rooms keyed by tenant / user / entity (a prefix convention), then emit to the room — so only the right clients get it, and tenants stay isolated.

// a user can be in several rooms: tenant:, user:, room:
this.server.to(`tenant:${orgId}`).emit('order.updated', payload);   // just this tenant
this.server.to(`user:${userId}`).emit('notification', payload);     // just this user

Reserve server.emit(...) (everyone) for true broadcasts; default to a room.

4. Scale across instances with a Redis pub/sub adapter (the #1 gotcha)

A socket lives on one instance. With >1 instance, server.to(room).emit() only reaches clients on that instance unless a pub/sub adapter relays emits to the others. Wire a Redis adapter; also require sticky sessions at the LB (the HTTP upgrade + polling fallback must return to the same pod).

export class SocketAdapter extends IoAdapter {                 // custom adapter
  private ctor!: ReturnType;
  async connectToRedis() {
    const pub = createClient({ /* host/port/tls from config */ }); const sub = pub.duplicate();
    await Promise.all([pub.connect(), sub.connect()]);
    this.ctor = createAdapter(pub, sub);                       // @socket.io/redis-adapter
  }
  createIOServer(port: number, opts?: ServerOptions) {
    const server = super.createIOServer(port, opts); server.adapter(this.ctor); return server;
  }
}

Without this, realtime "works in dev, drops half the messages in prod" (one pod locally, many pods deployed).

5. Bridge domain events → room emits (gateway is a transport, not a brain)

The gateway shouldn't contain business logic. A use-case emits a domain event; an event handler (or the service) calls the gateway to broadcast to the relevant room. Same events that drive the rest of the system drive realtime.

@EventsHandler(OrderStatusChangedEvent)
class PushOrderStatus { handle(e) { this.gateway.toRoom(`tenant:${e.orgId}`, 'order.updated', map(e)); } }

(Inbound cross-service triggers arrive via SQS/RPC — integrate-internal-services — then fan out to sockets.)

6. Reconnection & client contract

Clients drop and reconnect constantly (network, sleep). On reconnect the client re-sends its token and re-joins (your middleware re-runs, so rooms are restored). Emit typed, mapped payloads (a subset, like outbound events in write-service-code §6), version event names, and don't assume delivery — critical state must also be fetchable via a normal request. ▸ Other stacks: ws/Socket.IO (Node), Phoenix Channels (Elixir), ActionCable (Rails), SignalR (.NET), Centrifugo. The three invariants are universal: auth at connect, rooms/channels for targeting, a shared backplane (Redis/NATS) + sticky sessions to scale.

Verification

  • Auth at handshake, not per-message: open a socket with no auth.token → server emits error and disconnects (client connected === false); a valid token stays connected. grep -rn "verify\|introspect" src --include='*gateway*' finds the check in a server.use(...) middleware, not inside any @SubscribeMessage handler.
  • Rooms, not global: grep -rn "server\.emit(" src returns only deliberate broadcasts (≈0); targeted sends go through .to(room).emit(...). Connected as tenant B, emit to tenant A's room → B receives nothing.
  • Cross-instance backplane: run 2 instances, one client on each, join both to a room, emit once → both receive it. redis-cli PUBSUB CHANNELS "*socket.io*" is non-empty while clients are connected; the LB/ingress has session affinity (sticky) on.
  • Transport-only gateway: grep -rn "Repository\|DataSource" src --include='*gateway*' → empty (no DB in the gateway); emits originate from @EventsHandler classes.
  • Lifecycle hygiene: grep -n "removeAllListeners\|setMaxListeners" src hits handleDisconnect + bootstrap; drop a client's network then restore → it reconnects, the middleware re-runs, and a re-emit to its room reaches it.

Related

  • integrate-internal-services — the SQS/RPC events that feed the broadcasts (use-case → event → gateway emit).
  • integrate-identity-providers — verifying the handshake token; write-service-code §6 (mapped-subset payloads), §7 (logging).
  • background-jobs-and-caching — the Redis you already run also backs the socket adapter · containerize-and-ship-a-service (sticky-session ingress).

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.