Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wrpc

wrpc

npm CI node dependencies docs license

A fast, zero-dependency WebSocket-based RPC protocol for Node.js and browsers. Router and procedures, subscriptions that resume, rooms that scale across processes, and binary streams with backpressure that reaches all the way into TCP — under 15 KB min+gzip in a browser bundle, and nothing at all in your lockfile.

const { Server, WrpcClient, defineRouter, procedure } = require('@alexify/wrpc');

const router = defineRouter({
  greeting: {
    hello: procedure({
      access: 'public',
      handler: async (context, { name }) => `Hello, ${name}`,
    }),
  },
});

const server = new Server({ router, host: '127.0.0.1', port: 8000, protocol: 'http' });
await server.listen();

const client = await WrpcClient.connect('ws://127.0.0.1:8000/api');
await client.load('greeting');
await client.api.greeting.hello({ name: 'World' }); // → 'Hello, World'

📖 Full documentation · Getting started · Wire protocol

Why zero dependencies?

Nothing extra in the process handling your RPC traffic. package.json has no dependencies field at all — and no peerDependencies or optionalDependencies either, because those are dependencies with extra steps. The WebSocket engine is written from scratch (RFC 6455 + 7692), and the handful of utilities borrowed from elsewhere were copied in and adapted rather than installed.

Optional integrations are injected, not depended on. uWebSockets.js, Redis, fastify, express and TanStack Query all work — you require them and hand them over, and wrpc duck-types the injection at the boundary. So the integrations you do not use cost you nothing, and the ones you do use are on your version, not ours.

One protocol, both sides. The browser build contains zero Node builtins, so the client that talks to your server is the same implementation, not a reimplementation.

Positioning

wrpc tRPC Socket.IO
Transport WebSocket, HTTP, SSE, worker port HTTP, WebSocket WebSocket + long-poll fallback
Types contract-first + codegen, no build step TypeScript inference none built in
Realtime subscriptions, events, rooms, acks subscriptions events, rooms, acks
Binary streams with backpressure ❌ (messages only)
Scaling rooms backplane (any pub/sub) your own adapters
Cluster ops fetchClients, commands, presence (local read) your own fetchSockets (round-trip)
Runtime deps 0 a few several
Needs TypeScript effectively yes

When NOT to use wrpc

  • You want a public HTTP API. wrpc has a REST mode, but it is a convenience for reaching procedures, not an API design. If third parties consume it, write an HTTP API and document it.
  • Your stack is polyglot. The protocol is documented and small enough to reimplement, but the only implementation today is JavaScript. gRPC exists for a reason.
  • You need guaranteed delivery. Rooms are at-most-once fan-out, not a queue. Subscriptions with tracked() values and an event log let a client catch up; if you need a broker's guarantees, use a broker.
  • You want end-to-end type inference from server code. tRPC's model — where the client's types come from the server's implementation with nothing written twice — is genuinely nicer if your whole stack is TypeScript. wrpc's contract is declared or generated, deliberately, because it must also work for JavaScript users and across a network boundary the compiler cannot see.

Table of Contents

Installation

npm install @alexify/wrpc

Requires Node.js ≥ 22 (or any modern browser through a bundler). The package is CommonJS and ships as-is — no build step and no transpile; require('@alexify/wrpc') and import { Server } from '@alexify/wrpc' both work.

Bundle size

Measured with pnpm size (esbuild, fully minified with identifier mangling, then gzipped):

Entry min min+gzip budget
@alexify/wrpc — browser (client, streams, chunks) 44.3 KB 14.9 KB 15.0 KB
@alexify/wrpc — node (client + server) 158.2 KB 52.2 KB
@alexify/wrpc/ws (WebSocket engine) 20.7 KB 7.5 KB
@alexify/wrpc/engine (engine port) 21.2 KB 7.7 KB
@alexify/wrpc/uws (uWebSockets.js adapter) 16.0 KB 6.4 KB
@alexify/wrpc/fastify 135.3 KB 46.2 KB
@alexify/wrpc/express 122.9 KB 41.3 KB
@alexify/wrpc/scaling (rooms backplane) 4.1 KB 1.7 KB
@alexify/wrpc/sse — browser (client transport) 47.2 KB 15.9 KB 16.0 KB
@alexify/wrpc/sse — node 86.1 KB 28.6 KB
@alexify/wrpc/query (TanStack bindings) 2.7 KB 1.1 KB 2.0 KB
@alexify/wrpc/auth (token strategies) 3.3 KB 1.5 KB 2.0 KB

The Node-only rows are reported for visibility into what each subpath pulls in — they never ship to a browser, and the adapter rows include the whole core because they bundle it. Only the browser-reachable entries carry a budget, and exceeding one fails CI: it is a ratchet against accidental bloat, raised deliberately in the same change that earns the bytes.

Injected packages (uWebSockets.js, ioredis, fastify, express, @tanstack/query-core) are never bundled — you install what you use.

Browser usage

The package ships a browser entry (browser.js), resolved automatically by any bundler that honours the package.json browser field and the browser condition in exports — webpack 5, Vite, esbuild (platform: 'browser'), Parcel, Bun. Rollup users need @rollup/plugin-node-resolve with browser: true.

It contains WrpcClient/WrpcClientProxy, the stream and chunk helpers, and zero Node builtins — the server half is not in it. The two platform-specific pairs (chunks, runtime) are swapped automatically, and scripts/size.js fails the build if any browser entry ever resolves a node: import or a package.

Quick Start

A router of procedures, a server, a client:

const { Server, WrpcClient, defineRouter, procedure, tracked } = require('@alexify/wrpc');

const router = defineRouter({
  chat: {
    // a call
    send: procedure({
      access: 'session',
      input: (args) => { if (!args?.text) throw new Error('text is required'); },
      handler: async (context, { text }) => {
        const sent = context.server.to('lobby').emit('chat/message', { text });
        return { sent };
      },
    }),
    // a subscription: many values, one connection
    onMessage: procedure.subscription({
      access: 'session',
      handler: async function* (context, args, { lastEventId, signal }) {
        for await (const message of feed({ signal })) yield tracked(message.id, message);
      },
    }),
    // an inbound, fire-and-forget event
    on: {
      typing: procedure({ handler: async (context, data) => announce(data) }),
    },
  },
  auth: {
    login: procedure({
      access: 'public',
      handler: async (context, { user }) => {
        context.client.startSession(undefined, { user });
        return { ok: true };
      },
    }),
  },
});

const server = new Server({ router, host: '127.0.0.1', port: 8000, protocol: 'http' });
await server.listen();
const client = await WrpcClient.connect('ws://127.0.0.1:8000/api');
await client.load('chat', 'auth');

await client.api.auth.login({ user: 'ada' });
client.api.chat.on('message', ({ text }) => console.log(text));
client.sendEvent('chat/typing', { who: 'ada' });

for await (const message of client.api.chat.onMessage.iterate()) {
  console.log(message);
}

Features

Area What you get
RPC Router and procedures with access control, Standard Schema validators, timeouts, concurrency queues and versioned units
Realtime Events both ways, rooms, acks (client.ask, to(room).ask), subscriptions with resume, cancellation, call batching
Hooks Lifecycle phases in the fastify tradition — router, unit and procedure levels flattened into one pipeline; no (ctx, next) middleware
Streams Binary upload/download interleaved on one connection, with end-to-end backpressure
REST Declarative endpoints on the same procedures — verb, path, status, fastify-shaped schemas, /vN version paths, OpenAPI via wrpc types --openapi
Sessions & auth Cookie-backed sessions restored on reconnect, pluggable store, CSRF-aware REST dispatch; bearer/payload token carriers, client stores and the authenticate/refresh lifecycle
Scaling Rooms backplane over any pub/sub; Redis and in-memory adapters included; cluster layer — replicated presence (count with no round-trip), fetchClients, cross-instance commands, node-to-node ask
Transports WebSocket, plain HTTP, Server-Sent Events, Service Worker MessagePort; pluggable wire codec and request metadata (x-wrpc-meta-<key>)
Hosts Batteries-included server, or fastify / express / uWebSockets.js / bare node:http
Client Exponential backoff with full jitter, app-level heartbeat, automatic re-load() and re-subscribe, offline/online
Observability Structured logging into your pino, OpenTelemetry spans and metrics, W3C trace context across the wire
Operations Security hardening, graceful drain and shutdown, benchmarks you can reproduce, testing patterns
DX Contract-first typed client, wrpc types codegen, TanStack Query bindings, hand-maintained .d.ts for every subpath

Observability

Both are injected, never depended on — the package still has no dependencies. Your logger and your OpenTelemetry SDK are duck-typed.

const pino = require('pino');
const api = require('@opentelemetry/api');

const server = new Server({ router, logger: pino(), telemetry: { api } });

logger takes a structured logger (pino, bunyan, winston), a Console, or false to go silent. wrpc binds children for you — component, peer and a per-call callId — and context.log inside a handler is already scoped to that call.

telemetry takes the @opentelemetry/api module or your own { tracer, meter }. Spans follow the OTel rpc.* convention, fourteen metrics cover calls, connections, subscriptions, broadcasts and streams, and call packets carry W3C trace context so a client span parents the server's across the network hop.

Neither can break a request: a logger that throws, a broken exporter or a meter that dies is contained at the call site.

See Logging and OpenTelemetry.

Exports

Subpath Exports Docs
@alexify/wrpc Server, RpcServer, WrpcClient, connect, defineRouter, procedure, tracked, createEventLog, createEventStream, MemorySessionStore, WrpcReadable, WrpcWritable, WrpcError, chunkEncode/chunkDecode Server · Client
@alexify/wrpc/ws WebsocketServer, Connection, Frame, FrameParser, OPCODES, CLOSE_CODES Wire format
@alexify/wrpc/engine createNodeEngine, isEngine, the Engine/WrpcSocket contracts Engine port
@alexify/wrpc/uws createUwsEngine, UwsSocket uWebSockets.js
@alexify/wrpc/fastify wrpcFastify, findUwsApp Fastify
@alexify/wrpc/express createWrpc Express
@alexify/wrpc/scaling MemoryBackplane, createRedisAdapter, isBackplane Scaling
@alexify/wrpc/sse SseChannels, ServerSseTransport, ClientSseTransport, SseParser Server-Sent Events
@alexify/wrpc/query createQueryUtils TanStack Query
@alexify/wrpc/auth bearerAuth, memoryStore, webStorage, cookieStorage, bearerTransport, payloadTransport Authentication
wrpc (bin) wrpc types <url> --out api.d.ts Codegen CLI

Every subpath ships hand-maintained TypeScript declarations — no generation, no any where a real type belongs.

Documentation

Contributing

See CONTRIBUTING.md for the development workflow, the house rules (zero dependencies, .d.ts and tsd in the same change) and the release checklist.

pnpm test           # node --test, recursive
pnpm test:coverage  # c8, thresholds 95/95/90/95
pnpm test:types     # tsd
pnpm lint           # oxlint
pnpm size           # bundle-size budgets
pnpm docs:dev       # the documentation site

Changelog

See CHANGELOG.md.

Security

See SECURITY.md for how to report a vulnerability.

License

MIT — see LICENSE.

About

Fast, zero-dependency, WebSocket-based RPC protocol for Node.js and browsers: one router over WebSocket, HTTP, SSE and REST - typed client, subscriptions, rooms, cluster and binary streams

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages