Skip to content

Server-Sent Events

SSE is the browser-facing streaming transport built directly on the event bus and event definition model.

Use it when the server needs to push updates to clients but clients do not need a bidirectional socket.

These examples extend the starter app and the blog:post.published event from Events and the Event Bus.

app.config.ts
// @skip-typecheck
import { defineApp } from '@lastshotlabs/slingshot';
import { blogEventsPlugin } from './blog/events';
import { blogPackage } from './blog/package';
export default defineApp({
packages: [blogPackage],
plugins: [blogEventsPlugin],
sse: {
endpoints: {
'/events': {
events: ['blog:post.published'],
heartbeat: 15_000,
filter: (client, _event, payload) =>
client.actor.id !== null &&
(payload as { authorId: string }).authorId === client.actor.id,
},
},
},
});

An SSE endpoint does not publish events itself. It subscribes to the app event bus and streams events whose definitions allow that exposure.

That means SSE depends on:

  • event definitions
  • event envelope scope
  • client-safe exposure rules
  • optional endpoint-specific filtering

Only events that were explicitly defined and exposed should reach browser clients.

Typical flow:

  1. register an event with defineEvent(...)
  2. mark it client-safe
  3. publish it with ctx.events.publish(...)
  4. let SSE stream it to matching subscribers

This keeps browser delivery aligned with the same canonical event model used elsewhere.

web/post-events.ts
const source = new EventSource('/events');
source.addEventListener('blog:post.published', event => {
const payload = JSON.parse(event.data) as {
postId: string;
title: string;
slug: string;
};
console.log('new published post', payload.slug, payload.title);
});

Each endpoint can also define:

  • upgrade(req) to accept or reject the connection and attach client metadata
  • filter(client, event, payload) for per-client delivery control
  • heartbeat to keep connections alive

That gives you a clean split:

  • event definitions control what is externally exposable
  • endpoint filters control which subscribed client gets which exposed event
  • default: expose a client-safe event on one SSE endpoint
  • customize: add upgrade(req) or filter(client, event, payload) for finer per-client control
  • escape hatch: change the underlying event bus transport when SSE needs cross-instance delivery
  • notifications
  • activity feeds
  • dashboards
  • browser event streams where EventSource is enough

SSE is simpler because it is one-way. It does not replace:

  • room subscriptions
  • client-to-server events
  • presence
  • connection recovery for bidirectional sessions

Those belong to WebSockets.