Campus application sockets
Campus application sockets carry long-lived, bidirectional app traffic across the same trust boundary as ordinary CampusSDK requests:
App frontend
-> CampusSDK / Wasm
-> runcampus/socket/1 over Iroh
-> per-installation Campus Gateway
-> WebSocket on the private backend portThey are intended for agent event streams, chats, terminals, collaboration, live status, and other application protocols that do not fit buffered HTTP request/response calls.
Security contract
Opening a socket requires the launch descriptor's installation capability. Campus Gateway verifies that capability before resolving a path or opening a backend connection. The caller supplies only an absolute path such as /v1/events; schemes, hosts, protocol-relative paths, and paths that escape the configured backend origin are rejected.
The backend remains bound to the loopback-published container port selected by Campus Node Runtime. The frontend never receives that address. Socket support does not expose an internet listener and does not change the package's network.public grant.
Campus Gateway constructs the WebSocket handshake itself. It removes caller-supplied credentials, cookies, hop-by-hop headers, and all Sec-WebSocket-* handshake headers. Applications may supply ordinary metadata headers and an explicit subprotocol list. The backend's cookies and handshake proof are not returned to the frontend.
The private socket surface connects only an application frontend to its own backend. Cross-app calls remain buffered HTTP. A separate public WebSocket ingress is available only for package paths that are both declared with method WS in publicEndpoints and granted as an exact endpoints.public resource such as WS:/native. It never uses the launch capability.
Public WebSocket ingress
Campus Ingress accepts authorized public upgrades on the CampusOS-assigned application hostname:
wss://<app-id>--<opaque-public-id>.<public-ingress-domain>/<path>CampusOS persists that hostname's Gateway EndpointId and pushes the topology to Ingress over its authenticated control connection. Ingress selects the Gateway, and Gateway applies the same exact declaration and installation-grant checks before resolving the private backend.
Campus filters caller credentials and hop-by-hop handshake headers, forwards only valid requested subprotocols, limits messages to 16 MiB, and bounds frames received while the client-side upgrade is being completed. Text, binary, ping, pong, selected subprotocol, and valid close information are relayed. The backend hop receives an explicit forwarded-public marker so an upstream gateway cannot mistake internet ingress for a trusted loopback client.
CAMPUS_PUBLIC_INGRESS_ORIGIN supplies the deployment's externally reachable domain used when CampusOS creates topology mappings. Public clients use TLS at the Ingress edge. RunCampus native clients do not receive this public URL; after login they connect with the private EndpointId, endpoint ticket, and scoped capability in their launch result. An app with an authorized schema-version-4 direct UDP declaration may additionally receive its Runtime-published port descriptor; that native UDP path is independent of WebSocket ingress.
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 socket = await campus.backend.openSocket('/gateway', {
headers: { 'x-client-version': '1' },
protocols: ['campus-agent.v1'],
})
await socket.sendText('hello')
await socket.sendBinary(new Uint8Array([0, 1, 2]))
const message = await socket.receive()
// { type: 'text', data: '...' }
// { type: 'binary' | 'ping' | 'pong', data: Uint8Array }
// { type: 'close', code: number | null, reason: string }
await socket.close(1000, 'done')CampusSocket is also an async iterable. Only one receive() operation may be in flight; the high-level wrapper serializes sends in call order. Only one send may be in flight when using the raw Wasm socket directly. The API deliberately remains asynchronous; it is not a drop-in replacement for globalThis.WebSocket.
Wire contract
Socket traffic uses the runcampus/socket/1 ALPN, separate from buffered application HTTP. One Iroh bidirectional stream represents one socket. Every record is a four-byte big-endian length followed by a typed payload:
connectandconnectedrecords establish capability, path, headers, and subprotocol;- text is UTF-8;
- binary, ping, and pong payloads stay binary and are not base64 encoded;
- close records carry an optional WebSocket close code and UTF-8 reason.
The first record must be connect. No backend dial occurs before it succeeds. Messages are limited to 16 MiB, handshake records to 64 KiB, and ping/pong payloads to 125 bytes. Client close codes follow browser rules: 1000 or an application code from 3000 through 4999, with at most 123 bytes of reason text.
Iroh provides reliable ordered delivery, encryption, congestion control, and transport backpressure. Campus Gateway updates the installation's activity and byte counters as socket traffic flows.
Backend requirements
The package still declares one container port and must answer GET /health there for lifecycle readiness. WebSocket routes are served on that same port. A package that embeds another gateway on a private secondary port should place a small in-container adapter on its declared Campus port and proxy only the intended socket path internally.