Campus server-sent events
Campus server-sent events provide the one-way streaming surface between buffered REST requests and bidirectional WebSockets:
App frontend
-> CampusSDK / Wasm
-> runcampus/sse/1 over Iroh
-> per-installation Campus Gateway
-> GET text/event-stream on the private backend portCampus has two deliberately separate SSE planes:
backend.openEventStream()connects one app frontend to that app's optional private backend over Iroh.accountEvents.subscribe()uses the single cookie-authenticated/v1/eventsconnection owned by the trusted Campus shell. The shell multiplexes exact, permission-granted topics to isolated app frames over the source-bound CampusSDK channel. Apps never receive the account cookie or open a second CampusOS SSE connection.
Shared account events
const campus = createCampusApp({ appId: 'campus-calls' })
const events = await campus.accountEvents.subscribe(['calls:*'])
for await (const event of events) {
// { id, topic, data, createdAt }
if (event.topic === 'campus:reconcile') {
await reconcileFromCallsSnapshot()
continue
}
if (event.topic === 'calls:incoming') showIncomingCall(event.data.call)
}The app manifest must request events.subscribe for every exact topic or namespace:* prefix. CampusSDK registers the subscription with the shell; the shell checks the installed grant and fans out only matching records. Hidden retained frames may keep their subscription without creating another network connection. Closing the returned async iterable releases the shell subscription.
campus:reconcile is a shell control event delivered once after a subscription opens and after the shared SSE connection reconnects. It is delivered to every subscription without being requested as a manifest topic. On receipt, the app must reload the durable snapshots that own the state behind its requested topics. The control event is a boundary marker, not a replay of missed records.
The shell uses the same stream for its unread projection and browser/OS notification mirror. A reconnect is followed by durable service snapshots, so notifications and ringing calls are recovered even if a transient event was missed. Realtime account events are signals, not durable app storage.
Workflows that must observe an explicitly durable result can opt into an installation-scoped consumer instead:
const results = await campus.accountEvents.subscribe(['calls:*'], {
delivery: 'durable',
consumer: 'outbound-call-workflow',
ackMode: 'explicit',
})
for await (const event of results) {
await advanceWorkflow(event)
await results.ack(event)
}The consumer name is stable within one installation. CampusOS persists a separate delivery for each matching registered consumer before emitting the realtime hint, leases only one outstanding delivery at a time, and advances that consumer only after ack(). Closing or crashing the app does not acknowledge the event; after the bounded lease it is returned with a higher deliveryAttempt. The one-use acknowledgment token stays inside the SDK stream and is not exposed on the event object. Delivery is therefore at-least-once: a workflow should derive downstream idempotency keys from the stable event.id, commit those effects, and acknowledge only afterward.
Durability is selected by the event producer as well as the subscriber. It is reserved for terminal results and similar workflow boundaries; subscribing in durable mode cannot turn an ordinary high-volume realtime hint into a stored record. Calls currently makes calls:answered, calls:declined, calls:ended, and calls:expired durable. These delivery records are a bounded coordination ledger, not a replacement for the service that owns the underlying domain state.
No app event-signing key is required. Registration, claim, and acknowledgment are authenticated as the exact installed app by its existing service capability, and topic access is checked against that installation's events.subscribe grant. This also works for autonomously created installations without introducing a separate public-key enrollment directory.
A workload or Agent component uses the same contract through its adjacent Gateway:
const campus = CampusServicesClient.fromEnv()
await campus.registerDurableEventConsumer('outbound-call-workflow', ['calls:*'])
const delivery = await campus.claimDurableEvent('outbound-call-workflow')
if (delivery) {
await commitCallTransition(delivery.event)
await campus.acknowledgeDurableEvent(delivery.id, delivery.ackToken)
}The Gateway substitutes the installation's protected CampusOS service credential. The workload sees only its normal CAMPUS_GATEWAY_TOKEN; neither app signing keys nor an account session enter it.
Browser API
const { entries } = await fetch('/assets/campus_sdk/manifest.json').then((response) => response.json())
const { createCampusApp } = await import(entries.browser)
const campus = createCampusApp({ appId: 'campus-agent' })
const events = await campus.backend.openEventStream('/v1/progress', {
headers: { 'x-client-version': '1' },
})
for await (const event of events) {
// { type: 'message' | '<named event>', data: string, lastEventId: string }
console.log(event.type, event.data)
}The parser follows the SSE field model: comments are ignored, multiple data lines are joined with newlines, missing event names become message, UTF-8 is decoded with replacement, and id and retry update reconnection state. The browser wrapper reconnects by default. It waits for the server-provided retry interval (three seconds initially), then reopens the route with the last event ID. Set reconnect: false for a finite stream or call close() to stop it.
The API is an async iterable rather than a drop-in globalThis.EventSource. Only one receive() operation may be in flight.
Backend SSE security contract
Opening an event stream requires the installation capability from the launch descriptor. Campus Gateway validates it before resolving the path or dialing the backend. Only an absolute path on the installation's configured backend origin is accepted; the frontend never receives the private loopback address.
Campus Gateway creates the GET itself with Accept: text/event-stream and Cache-Control: no-cache. Caller-supplied authorization, cookies, hop-by-hop headers, WebSocket headers, and Last-Event-ID are removed. The SDK supplies the last event ID through the authenticated wire handshake, preventing duplicate or conflicting headers. Backend cookies are not returned to the frontend.
SSE connects only a frontend to its own backend. It does not add a public listener and does not change MCP or publicEndpoints behavior.
Wire and lifecycle contract
The runcampus/sse/1 ALPN is separate from buffered HTTP and WebSockets. One Iroh bidirectional stream carries a capability/path handshake followed by binary-safe chunks and an end record. The client finishes its send direction after the handshake; Campus Gateway cancels the backend GET when the Iroh connection closes.
Handshake records are limited to 64 KiB. Individual chunks and parsed events are limited to roughly 16 MiB to bound memory. Iroh supplies encryption, reliable ordered delivery, flow control, and backpressure. Gateway request, byte, and activity counters remain active for the stream's lifetime.
The backend must serve SSE and GET /health on the package's declared container port. Proxying to a secondary internal service remains the application's responsibility.