One contract for every pattern on the wire — requests, events, and subscriptions — with end-to-end types and zero codegen. Run the same code over WebSocket, HTTP, or libp2p/WebRTC — the transport is one line — on a single node or a cluster.
pnpm add @super-line/core @super-line/server @super-line/client @super-line/transport-websocket
// one contract — imported by both sides
const chat = defineContract({
shared: {
clientToServer: {
send: {
input: z.object({ text: z.string() }),
output: z.object({ id: z.string() }),
},
},
serverToClient: {
message: {
payload: z.object({ text: z.string() }),
},
online: {
payload: z.object({ count: z.number() }),
subscribe: true,
},
},
},
roles: { user: {} },
})
// client — typed end to end, zero codegen
await client.send({ text: 'hi' }) // req/res
client.on('message', render) // event
client.subscribe('online', setOnline) // topic A connection from ws. An EventEmitter for local events. Redis pub/sub, hand-wired, when it has to cross processes. Correlation IDs and ack callbacks for request/response. Four moving parts, none of them typed across the wire — re-assembled on every project.
wsraw transportEventEmitterlocal eventsredispub/sub fan-outack gluereq/res by handThe contract is one object both ends import, so an event name or payload can't drift between client and server — change it in one place and TypeScript flags every call that no longer fits.
And types aren't trust. Every inbound message is validated against the same schema at runtime, so even an untyped peer can't slip a bad payload past the server.
srv.implement({
shared: {
// `text` is validated before your handler runs
send: async ({ text }, ctx) => {
srv.room('lobby').broadcast('message', { text })
return { id: crypto.randomUUID() } // typed reply
},
},
})New Pluggable transports
super-line splits what travels — your typed contract — from how it travels. The same server, the same client, the same handlers run over a WebSocket, an HTTP/SSE stream, or a libp2p/WebRTC peer connection. The transport is one line; everything above it is identical.
WebSocketfull-duplex · lowest latency · the default
// one client — identical on every wire
const client = createSuperLineClient(chat, {
transport: webSocketClientTransport({ url }), // ← the only line that changes
role: 'user',
})
await client.send({ text: 'hi' }) // same call, every wire Real: examples/transports — one server mounts WebSocket, HTTP and libp2p at once; three clients call the same echo, each over a different wire.
Transport — the client ↔ server wire this section
WebSocketHTTPlibp2ploopback
Adapter — the server ↔ server fan-out next ↓
Redislibp2pRabbitMQZeroMQ
Two instances behind a load balancer, and a message published on node A never reaches the client on node B. The usual fix is a pub/sub backbone you wire by hand, plus code to tell your own events from your peers'.
Pass an adapter. The same publish now fires in-process subscribers with no network hop and every other node across the backbone — meta.from tells you where each event came from. Redis ships today, and the adapter is just an interface — so libp2p, ZeroMQ, or your own drops in.
Real: examples/bus-cluster — three nodes converge a shared tally purely over the bus.
const srv = createSuperLineServer(bus, {
server,
adapter: createRedisAdapter(url), // one line → a cluster
})
srv.subscribe('bump', (b, { from }) => {
if (from === srv.nodeId) return // local echo, no hop
tally[b.node] += 1 // converge cluster state
})
srv.publish('bump', { node: NODE }) // reaches every node Flip on inspector: true and point Control Center at any node. It draws your live topology, every connection with its ctx, the running contract, and a streaming event feed — cluster-wide, with no instrumentation to add.
npx @super-line/control-center
super-line takes three positions and holds them — so you don't re-litigate them per feature.
One object, split by direction and scoped by role. Types flow to both ends; a cross-role call gets NOT_FOUND.
Every inbound message is validated against its schema before a handler ever sees it. Always on, not opt-in.
Clients don't self-join or self-subscribe. Membership and authorization live on the server, where they belong.
It's a typed distributed event emitter — and req/res, rooms, presence, and a server that's in charge.
| Capability | super-line | Socket.IO | tRPC | raw ws | dist. emitter |
|---|---|---|---|---|---|
| One typed contract (SSOT) | yes | types onlypartial — types only | yes | no | no |
| Runtime validation | yes | no | yes | no | no |
| Req/res — both directions | yes | ack cbspartial — ack cbs | c→s onlypartial — c→s only | no | no |
| Events & rooms | yes | yes | no | no | eventspartial — events |
| Topics (pub/sub) | yes | via roomspartial — via rooms | subspartial — subs | no | yes |
| Pluggable transport (WS · HTTP · libp2p) | yes | WS + pollpartial — WS + poll | link-basedpartial — link-based | no | no |
| Cross-node fan-out | yes | yes | no | no | yes |
| Per-role contracts | yes | no | no | no | no |
| Presence / introspection | clusteryes — cluster | roomspartial — rooms | no | no | no |
| Server-authoritative | yes | partial | no | no | no |
Requests, events, and subscriptions over one typed connection, zero codegen — on WebSocket, HTTP, or libp2p, with reconnection, presence, and a cluster event bus built in. Swap the wire in one line; add an adapter only when you outgrow a single node.
Pre-1.0 — role-scoped contracts, req/res, events, rooms, topics, the cluster event bus, presence, and reconnect are implemented and tested — over pluggable transports (WebSocket, HTTP, libp2p, loopback) and pluggable adapters (in-memory, Redis, libp2p, RabbitMQ, ZeroMQ).