Skip to content

WebSockets

WebSockets are a first-class core transport in Slingshot.

They are not interchangeable with SSE. Use WebSockets when clients need to send messages back, subscribe to rooms, or participate in presence and recovery flows.

These examples extend the starter app with a blog collaboration endpoint.

app.config.ts
// @skip-typecheck
import { defineApp } from '@lastshotlabs/slingshot';
import { blogPackage } from './blog/package';
export default defineApp({
packages: [blogPackage],
ws: {
endpoints: {
'/ws/blog': {
presence: true,
heartbeat: true,
rateLimit: { windowMs: 10_000, maxMessages: 50, onExceeded: 'close' },
onRoomSubscribe: async (ws, room) =>
room === `post:${ws.data.actor.id}:drafts` || room.startsWith('post:public:'),
incoming: {
'post:typing': {
auth: 'userAuth',
handler: (_ws, payload, ctx) => {
const room = `post:${ctx.actor.id}:drafts`;
ctx.publish(room, { type: 'post:typing', payload });
return { delivered: true };
},
},
'post:subscribePublic': {
auth: 'none',
handler: (_ws, payload, ctx) => {
const postId = (payload as { postId: string }).postId;
ctx.subscribe(`post:public:${postId}`);
return { subscribed: postId };
},
},
},
},
},
},
});

Each endpoint can configure:

  • on.open, on.message, on.close, on.drain
  • onRoomSubscribe
  • incoming named event handlers
  • middleware for incoming events
  • presence
  • heartbeat
  • persistence
  • recovery
  • rateLimit

That is a real runtime surface, not just a thin socket wrapper.

Incoming event handlers get a WsEventContext with:

  • publish(room, data)
  • subscribe(room)
  • unsubscribe(room)
  • socketId
  • actorId
  • endpoint

That is the canonical room-level API for framework-managed WebSocket behavior.

Presence and recovery are separate concerns:

  • presence tracks connected actors and room membership
  • recovery lets clients reconnect and resume within a configured window

Recovery requires persistence. Slingshot will reject invalid recovery configuration at startup instead of failing later at runtime.

Single-instance room publish works without extra setup. Multi-instance delivery requires a shared WebSocket transport:

app.config.ts (excerpt)
import { defineApp } from '@lastshotlabs/slingshot';
declare const myWsTransport: import('@lastshotlabs/slingshot').WsConfig['transport'];
export default defineApp({
ws: {
transport: myWsTransport,
endpoints: {
'/ws': { presence: true },
},
},
});

This is separate from the event bus. The event bus handles app events. The WebSocket transport handles cross-instance room fan-out.

  • default: one endpoint with incoming, onRoomSubscribe, and room publish
  • customize: add middleware, persistence, and recovery
  • escape hatch: supply a shared transport and let packages self-wire endpoint behavior through the bootstrap endpoint map

Packages can bootstrap endpoint hooks through the root ws config. createApp() exposes the bootstrap endpoint map on context so packages can attach incoming handlers or subscribe guards before the runtime starts the live transport.

That is how package-owned realtime behavior can remain package-owned instead of leaking into the app root.

  • chat and collaborative editing
  • room subscriptions
  • presence
  • low-latency bidirectional workflows

Use Server-Sent Events when the stream is server-to-client only.