Skip to content

Realtime

You want your UI to update the moment something changes on the server. A new message appears in the chat. A task gets completed and the board updates. A live cursor shows where another user is typing.

Slingshot has two built-in transports for this. Pick the one that matches your use case.

You want to…Use
Show live updates when data changes (dashboard, feed, notifications)SSE
Build chat, collaborative editing, or live cursorsWebSockets
Stream event-bus events to browser clientsSSE
Manage rooms, presence, or connection stateWebSockets
Keep infrastructure simpleSSE

SSE streams server events to browser clients over a simple HTTP connection. It plugs directly into the event bus — publish an event, mark it as client-safe, and browsers receive it automatically.

// @skip-typecheck
import { defineEvent } from '@lastshotlabs/slingshot';
// Mark events as client-safe so they reach browsers
events.register(
defineEvent('task.completed', {
ownerPlugin: 'app',
exposure: ['client-safe'],
resolveScope(payload) {
return { userId: payload.userId };
},
}),
);

Browser side:

const es = new EventSource('/events', { withCredentials: true });
es.onmessage = event => {
const data = JSON.parse(event.data);
console.log('Live update:', data);
};

That is it. No WebSocket server, no rooms, no connection management. The event bus does the heavy lifting.

See Server-Sent Events for the full setup including auth, scoping, and filtering.

WebSockets are for features where clients send messages back. Chat, collaborative editing, live cursors, multiplayer — anything that needs low-latency two-way communication.

app.config.ts
// @skip-typecheck
import { defineApp } from '@lastshotlabs/slingshot';
declare function verifyToken(token: string): string | null;
export default defineApp({
port: 3000,
ws: {
endpoints: {
'/ws/chat': {
upgrade: async req => {
const auth = req.headers.get('authorization');
const token = auth?.startsWith('Bearer ') ? auth.slice('Bearer '.length) : null;
if (!token) return new Response('Unauthorized', { status: 401 });
const userId = verifyToken(token);
if (!userId) return new Response('Unauthorized', { status: 401 });
return { actorId: userId };
},
on: {
open: ws => {
ws.subscribe('general');
},
message: (ws, message) => {
ws.publish('general', message);
},
close: ws => {
ws.unsubscribe('general');
},
},
},
},
},
});

Browser side:

const ws = new WebSocket('ws://localhost:3000/ws/chat');
ws.onmessage = event => console.log('Message:', event.data);
ws.send(JSON.stringify({ text: 'Hello everyone' }));

Bun’s built-in pub/sub handles rooms. Clients subscribe to named rooms and publish to them without any external dependencies.

See WebSockets for rooms, presence, auth guards, entity channels, and scaling.

SSE is tightly coupled to the event model. You publish events and SSE exposes selected ones to browsers.

WebSockets are a separate transport. They have their own connection lifecycle, rooms, and message handling. They can participate in the event model, but they are not just “events over another wire.”

  • SSE scales with your event bus. Switch to BullMQ or Redis and SSE events cross instance boundaries automatically.
  • WebSockets need explicit planning. Bun’s pub/sub is in-process — for multi-instance deployments, add a Redis transport.

Read Horizontal Scaling when you move beyond a single instance.

Realtime is online. Most apps also have work that should run outside the request cycle — emails, exports, retries, scheduled cleanup: