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 zod
A real two-node cluster runs on this page — watch a message cross the bus// 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. A job runner and its own datastore the moment work has to outlive the connection that asked for it. Five moving parts, none of them typed across the wire — re-assembled on every project.
wsraw transportEventEmitterlocal eventsredispub/sub fan-outack gluereq/res by handbullmqbackground jobs & cronThe 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 Collections
The same contract that types your messages also declares collections — typed rows you filter and subscribe to in subsets, and CRDT documents whose concurrent edits merge. Every write is schema-validated at the server before it commits.
super-line is the server-authoritative sync source: it owns the data, enforces row-level security, and streams each client exactly the slice it's allowed to see. One node or a cluster, it converges.
@super-line/collections-memory · -sqlite · -pglite — plus CRDT tiers. Collections →
// the SAME contract declares state: rows + a CRDT doc
const api = defineContract({
collections: {
messages: { schema: Message, key: 'id' },
canvas: { schema: Canvas, crdt: { mode: 'document' } },
},
roles: { user: {} },
})
// a live, filtered row-set — server-authoritative
const sub = client.collection('messages')
.subscribe({ filter: eq('room', 'lobby') })
// …and a document whose edits merge everywhere
const doc = client.collection('canvas').open('board')
doc.update({ title: 'hi' }) // converges across tabs + nodesNew Queue plugin
Some work can't finish inside a request. Declare a queue and its worker and it becomes a durable, at-least-once job: typed input and result, retries, leases, declarative concurrency — and cron, when it should just happen every morning.
Jobs are rows in the same collections as the rest of your state, so a job and the row that caused it commit together — no second datastore, no separate worker deployment. Point it at Postgres and the whole cluster coordinates.
@super-line/plugin-queueQueues & workers →
// the work and its worker, declared once
const jobs = queue({
queues: {
sendEmail: {
input: z.object({ to: z.email() }),
result: z.object({ messageId: z.string() }),
concurrency: 3, // enforced by durable slot rows
worker: async ({ to }, { signal }) => ({
messageId: await sendEmail(to, { signal }),
}),
},
},
})
// durable: survives a restart, retries, at least once
await jobs.enqueue('sendEmail', { to: 'ada@example.com' })
// …or every morning at 09:00, cluster-wide
await jobs.schedules.create({
queue: 'sendEmail',
cron: '0 9 * * *',
input: { to: 'ada@example.com' },
})New Chat plugin
Whole domains drop onto your contract as plugins. chatContract() adds channels, membership, and messages — every mutation server-authoritative and hookable — and chatAgentTools() hands that same typed surface to an LLM over its own connection, so the server authorization-checks every move. The agent is just another user on the bus.
// one contract — merge whole domains as plugins
const app = defineContract({
roles: { user: {} },
plugins: [authContract(), chatContract()],
})
// server — each plugin owns its policies + handlers
plugins: [authKit.plugin, chatKit.plugin],
// an AI agent is just a user — on the same wire,
// with the same typed surface, server-authorized
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5',
tools: chatAgentTools(client), // its own connection
})connecting a live super-line instance…
That panel is live: the real plugin running in this tab over the loopback transport — every message you send is a typed request, and the agent replies through the same server-authoritative API. The full app (with a real LLM agent) is examples/collections-chat. Chat plugin →
New Channel resources
In examples/chat-supervisor, a human and a Mastra supervisor agent co-edit one CRDT canvas through a chat channel — in the browser, in a full terminal cockpit, and as a headless JSONL line protocol a script can drive. Same contract, same hooks — three renderers.

The cockpit mounts the same React hooks the web app uses — useMessages, useChannelResources, the live doc handle — under a terminal renderer, and pnpm tui --json strips the UI entirely. The example → · The cockpit →
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
A real super-line cluster, running in this tab: two server nodes joined by one adapter, two subscriber clients each. React on any client — it fans out to that node's other client and crosses the bus to the far node. Sever the bus and cross-node delivery stops dead, while each node keeps serving its own.
Mount the inspector() plugin 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 |
| Typed persisted state (rows + CRDT) | yes | no | no | no | no |
| Durable background jobs & cron | yes | no | 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, reconnect, collections (typed rows + CRDT documents), durable queues with cluster-wide cron, and plugins (auth, queue, chat, inspector) are implemented and tested — over pluggable transports (WebSocket, HTTP, libp2p, loopback) and pluggable adapters (in-memory, Redis, libp2p, RabbitMQ, ZeroMQ).