A WebSocket relay that connects agent handlers (running in Node.js) with browser clients. Handlers register with the relay server and expose an async generator interface. Browser clients connect over WebSocket to invoke handlers, stream responses, and manage sessions with abort/undo/redo support.
This is useful when you need to bridge a long-running backend process (like an AI agent, build tool, or CLI) with a browser frontend over WebSocket, with built-in session management, reconnection, and multiplexing.
npm install public-transit┌─────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────┐
│ Browser │ ◄───────────► │ Relay Server │ ◄───────────► │ Handler │
│ (client) │ │ (hub) │ │ (agent) │
└─────────┘ └──────────────┘ └─────────┘
- A relay server runs on a known port (default
4722) - Handlers connect and register themselves by ID (e.g.
"my-agent") - Browser clients connect and can invoke any registered handler
- Messages stream back from handler → relay → browser in real time
The simplest way — connectRelay auto-starts a relay server if one isn't already running, then registers your handler:
import { connectRelay } from "public-transit";
const connection = await connectRelay({
handler: {
agentId: "my-agent",
run: async function* (prompt, options) {
yield { type: "status", content: "Thinking..." };
// do work...
yield { type: "done", content: "Here's the result." };
},
abort: (sessionId) => {
// cancel in-progress work
},
},
});
// later: connection.disconnect()import { createRelayClient } from "public-transit/client";
const client = createRelayClient();
await client.connect();
// Listen for handler availability
client.onHandlersChange((handlers) => {
console.log("Available handlers:", handlers);
});
// Send a request
client.sendAgentRequest("my-agent", {
prompt: "Refactor this component",
content: ["const App = () => <div>hello</div>"],
});
// Stream responses
client.onMessage((message) => {
if (message.type === "agent-status") {
console.log("Status:", message.content);
} else if (message.type === "agent-done") {
console.log("Done:", message.content);
} else if (message.type === "agent-error") {
console.error("Error:", message.content);
}
});For a more ergonomic async-iterable interface:
import { createRelayClient, createRelayAgentProvider } from "public-transit/client";
const client = createRelayClient();
await client.connect();
const agent = createRelayAgentProvider({
relayClient: client,
agentId: "my-agent",
});
const controller = new AbortController();
for await (const chunk of agent.send(
{ prompt: "Fix the bug", content: ["..."] },
controller.signal,
)) {
console.log(chunk);
}If you want to run the relay server separately from handlers:
import { createRelayServer } from "public-transit/server";
const server = createRelayServer({ port: 4722, token: "secret" });
await server.start();
// Handlers and browsers connect over WebSocket
// Token is validated on connection| Path | Platform | Description |
|---|---|---|
public-transit |
Node.js | Everything (server + client + protocol) |
public-transit/server |
Node.js | createRelayServer |
public-transit/client |
Browser | createRelayClient, createRelayAgentProvider |
public-transit/protocol |
Any | Shared types and constants |
Pass a token option to the server and clients. The relay validates the token on both HTTP health checks and WebSocket connections:
// Server
createRelayServer({ token: "secret" });
// Handler
connectRelay({ handler, token: "secret" });
// Browser
createRelayClient({ token: "secret" });MIT