Skip to content

The strictly typed, opinionated data bus for TypeScript.

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

contract.ts + client.ts
// 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

Today, realtime is a glue job.

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 transport
  • EventEmitterlocal events
  • redispub/sub fan-out
  • ack gluereq/res by hand
super-lineone contract · one connection · strictly typed

Rename a field. The other side stops compiling.

The 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.

server.ts
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

Same app. Any wire.

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.

@super-line/transport-websocket
// 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
client → websocketsend({ text: 'hi' })… on the 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

It works on one node. Then you add a second.

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.

node.ts
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
node-1bump node-1 (origin self)total 4
node-2bump node-1 (origin a1b2c3d4)total 4
client← cluster total 6{ n1:2, n2:2, n3:2 }

See the whole network.

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 · Control Center

Opinionated, on purpose: the server is in charge.

super-line takes three positions and holds them — so you don't re-litigate them per feature.

  1. The contract is the source of truth

    One object, split by direction and scoped by role. Types flow to both ends; a cross-role call gets NOT_FOUND.

  2. Nothing on the wire is trusted

    Every inbound message is validated against its schema before a handler ever sees it. Always on, not opt-in.

  3. The server owns rooms & topics

    Clients don't self-join or self-subscribe. Membership and authorization live on the server, where they belong.

One library where you'd otherwise reach for several.

It's a typed distributed event emitter — and req/res, rooms, presence, and a server that's in charge.

Capability comparison of super-line against Socket.IO, tRPC, raw ws, and distributed event-emitter libraries
Capabilitysuper-lineSocket.IOtRPCraw wsdist. emitter
One typed contract (SSOT)yestypes onlypartial — types onlyyesnono
Runtime validationyesnoyesnono
Req/res — both directionsyesack cbspartial — ack cbsc→s onlypartial — c→s onlynono
Events & roomsyesyesnonoeventspartial — events
Topics (pub/sub)yesvia roomspartial — via roomssubspartial — subsnoyes
Pluggable transport (WS · HTTP · libp2p)yesWS + pollpartial — WS + polllink-basedpartial — link-basednono
Cross-node fan-outyesyesnonoyes
Per-role contractsyesnononono
Presence / introspectionclusteryes — clusterroomspartial — roomsnonono
Server-authoritativeyespartialnonono

One bus. Every pattern. Any wire.

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.

npmpnpm add @super-line/core @super-line/server @super-line/client zod

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).

Released under the MIT License.