@molecule/api-realtime-sse
Provider bond · realtime · API (Node) · v1.0.2 · Apache-2.0
Server-Sent Events (SSE) realtime provider for molecule.dev
npm install @molecule/api-realtime-ssenpm · Source on GitHub · Implements @molecule/api-realtime
How it works
@molecule/api-realtime-sse is a provider bond on the API (Node) side: it implements the realtime core interface (@molecule/api-realtime) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import http from 'node:http'
import { createProvider } from '@molecule/api-realtime-sse'
import { setProvider } from '@molecule/api-realtime'
// Attach the SSE endpoints to the API's own HTTP server so realtime
// shares the API port (a standalone `port` binds a SECOND port that a
// containerized/proxied deployment usually does not expose — and the
// default port 3000 collides with the typical API port).
const server = http.createServer()
const sseProvider = createProvider({ httpServer: server, path: '/sse' })
// Bond it as the active realtime provider
setProvider(sseProvider)
server.listen(3000)
// When the HTTP server doesn't exist yet at wiring time (e.g. a server
// factory that creates it later), defer instead of passing httpServer:
// const sseProvider = createProvider({ deferAttach: true, path: '/sse' })
// setProvider(sseProvider)
// // once the server exists (e.g. a server-created hook):
// sseProvider.attachHttpServer(server)Works with: @molecule/api-bond, @molecule/api-realtime
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
SSE (Server-Sent Events) realtime provider for molecule.dev.
Provides a RealtimeProvider implementation using native Node.js
Server-Sent Events for server-to-client push, with HTTP POST for
client-to-server messages.
- Client wire protocol (for hand-rolled clients): subscribe with
GET {path}— the first stream event isconnected{ clientId }; keep thatclientId, everyPOST {path}body must include it (400 without it). Handshake auth for join guards = every subscribe query param exceptroom/rooms, plus theAuthorizationheader (asauth.authorization); auth is captured once at subscribe and reused for later POSTed joins. Join rooms at subscribe time with?room=a&room=bor later viaPOST {clientId, event: 'molecule:join', data: {room}}— the verdict (molecule:joined/molecule:join-denied) arrives on the STREAM; the POST itself acks202.molecule:room-sendinto a room the client hasn't joined is rejected403. broadcast()throwsRoom "<id>" does not existwhen the room matches no managed room and no protocol room — and a protocol room ceases to exist when its last member leaves/disconnects. Guard server-side pushes accordingly (the-socketiobond silently no-ops instead).
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import http from 'node:http'
import { createProvider } from '@molecule/api-realtime-sse'
import { setProvider } from '@molecule/api-realtime'
// Attach the SSE endpoints to the API's own HTTP server so realtime
// shares the API port (a standalone `port` binds a SECOND port that a
// containerized/proxied deployment usually does not expose — and the
// default port 3000 collides with the typical API port).
const server = http.createServer()
const sseProvider = createProvider({ httpServer: server, path: '/sse' })
// Bond it as the active realtime provider
setProvider(sseProvider)
server.listen(3000)
// When the HTTP server doesn't exist yet at wiring time (e.g. a server
// factory that creates it later), defer instead of passing httpServer:
// const sseProvider = createProvider({ deferAttach: true, path: '/sse' })
// setProvider(sseProvider)
// // once the server exists (e.g. a server-created hook):
// sseProvider.attachHttpServer(server)
Type
provider
Installation
npm install @molecule/api-realtime-sse @molecule/api-bond @molecule/api-realtime
API
Interfaces
SseRealtimeConfig
Configuration options for the SSE realtime provider.
SSE (Server-Sent Events) provides server-to-client push. Client-to-server
messages are accepted via HTTP POST on the same path (configurable via
path).
interface SseRealtimeConfig {
/**
* An existing Node.js HTTP server to attach the SSE routes to. This is
* itself an explicit attach step — when given, the routes are attached
* immediately (or a standalone server is created on `port`
* immediately if omitted, per the rules below), regardless of
* `deferAttach`.
*/
httpServer?: HttpServer
/**
* Port to listen on for a standalone server. Passing this **explicitly**
* is an explicit instruction to bind a standalone server immediately (no
* `httpServer` needed) — env-aware for the actual value: `SSE_PORT` if
* set, else `PORT + 1000` (one above the API convention), else `3000`, so
* multiple apps can run side-by-side.
*
* **Omitting `port` (along with `httpServer` and `deferAttach`) does NOT
* bind a default port** — creating a provider must never bind a port as a
* side effect. A zero-config `createProvider()` behaves exactly like
* `{ deferAttach: true }` instead: it waits for
* `RealtimeProvider.attachHttpServer` and logs an info line naming
* the bond so the omission is visible.
*/
port?: number
/**
* Defer attaching the SSE routes until `RealtimeProvider.attachHttpServer`
* is called, instead of binding a standalone HTTP server eagerly at
* creation. Used by the server factory so SSE attaches to the API's HTTP
* server (shared port) once it exists — avoiding a standalone port a
* sandbox/proxy may not expose and that collides with the API's own port
* by default. Ignored when `httpServer` is already provided (that is
* itself an explicit attach — see the module `@remarks`). Zero-config
* (no `port`, no `httpServer`, no `deferAttach`) already behaves as if
* this were `true` — set it explicitly for readability/intent at the call
* site, not because it changes behavior over omitting it.
*
* @defaultValue false
*/
deferAttach?: boolean
/**
* Base path for SSE endpoints.
*
* - `GET {path}` — SSE event stream
* - `POST {path}` — client-to-server messages (`{ clientId, event, data, room? }`;
* `clientId` is REQUIRED — it arrives in the stream's initial `connected` event)
*
* @defaultValue '/sse'
*/
path?: string
/**
* Interval (ms) between keep-alive comment lines sent to prevent
* intermediary proxies from closing idle connections.
*
* @defaultValue 30000
*/
keepAliveInterval?: number
/**
* Custom headers to include in the SSE response.
*/
headers?: Record<string, string>
/**
* CORS origin for the SSE endpoint (`Access-Control-Allow-Origin`). Set to
* `'*'` to allow all origins, or a single origin string for a locked-down
* deployment.
*
* When omitted: outside production, defaults to `'*'` (dev convenience —
* no cross-origin risk to a real user). **In production** (`NODE_ENV ===
* 'production'`), defaults instead to `process.env.APP_ORIGIN ??
* process.env.SITE_ORIGIN` (the same env vars `@molecule/api-middleware-cors-express`
* reads for its allowlist) when either is set, so the realtime endpoints
* are NOT exposed cross-origin by default in production. If production AND
* neither is set, falls back to `'*'` but logs an actionable warning (auth
* via query params/`Authorization` header still applies, and
* credentialed-CORS rules still protect httpOnly-cookie flows, so exposure
* is limited — but should still be closed by setting this explicitly).
*
* @defaultValue '*' outside production; `APP_ORIGIN`/`SITE_ORIGIN` in production when set
*/
corsOrigin?: string
}
Functions
createProvider(config)
Creates an SSE-backed RealtimeProvider.
function createProvider(config?: SseRealtimeConfig): RealtimeProvider
config— SSE provider configuration.
Returns: A fully initialised RealtimeProvider backed by Server-Sent Events.
Core Interface
Implements @molecule/api-realtime interface.
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-realtime^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-realtime -
createProvider()with NOport, NOhttpServer, and NOdeferAttachdoes NOT bind anything — creating a provider must never bind a port as a side effect. It behaves exactly like{ deferAttach: true }(waits forattachHttpServer(server)), logging an info line so the omission is visible instead of silent. An explicitportstill binds a standalone server immediately (unchanged, back-compat for existing standalone callers) — it just no longer happens by accident. A standalone bind failure (e.g.EADDRINUSE) is logged via the bonded logger naming this bond and the port, instead of crashing the process with an unattributed error. -
corsOrigindefaults to'*'outside production. In production (NODE_ENV === 'production') it instead defaults toprocess.env.APP_ORIGIN ?? process.env.SITE_ORIGINwhen either is set, so the realtime stream/message endpoints aren't exposed cross-origin by default; only when neither is configured does it fall back to'*', logging a warning naming the risk. SetcorsOriginexplicitly to override either way.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- With the app open in TWO sessions (separate browser contexts/users), an action in one (send a message, update a shared record) appears in the other WITHOUT a manual reload.
- Updates reach only the sessions in the same room/scope — a session viewing a different room/record receives nothing.
- Private rooms enforce the join guard: an unauthorized session's join is
denied and no data leaks to it. If ANY client can join any private room,
onJoinRequestwas never registered — an integration bug. - Presence (if surfaced) updates when a participant joins and leaves.
- After a dropped connection (offline/online toggle or server restart), the client reconnects and live events flow again.