Skip to content
Alexis-TechnologiesPublic

About

wRPC (Web RPC): a fast, zero-dependency RPC protocol for Node.js and browsers - one router over WebSocket, HTTP, SSE, REST, WebTransport and WebRTC, plus message brokers - typed client, subscriptions, rooms, cluster, binary streams, compression and encryption

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wRPC

wRPC

npm CI node dependencies docs license

wRPC (Web RPC) is a fast, zero-dependency RPC protocol for Node.js and browsers: one router of procedures, served over every transport the web has — WebSocket, HTTP and REST, Server-Sent Events, WebTransport and WebRTC — and between services over Redis, NATS, RabbitMQ or Kafka. Subscriptions that resume, rooms that scale across processes, binary streams with backpressure that reaches all the way into TCP, compression and encryption where you want them — under 28 KB min+gzip in a browser bundle, a budget CI enforces, 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 (@alexify/wrpc) tRPC Socket.IO gRPC / Connect wRPC (Bytecode Alliance) webrpc
Transports WebSocket, HTTP + REST, SSE, WebTransport, WebRTC, worker port, message broker HTTP, SSE, WebSocket WebSocket, long-polling, WebTransport HTTP/2; Connect: HTTP/1.1–3 TCP, QUIC, WebTransport, Unix sockets HTTP POST
Peer to peer WebRTC data channels — — — — —
Contract / types declared or generated, no build step inferred from the router hand-written generics .proto + codegen WIT + codegen RIDL + codegen
Realtime subscriptions (resume), events, rooms, acks subscriptions (resume) events, rooms, acks streaming RPCs streams and futures server-streaming
Binary bytes in any packet, streams with backpressure binary inputs bytes in events protobuf bytes list<u8>, stream<u8> file up/download
Message brokers feeds, consumers, RPC (experimental) — a broadcast backplane — NATS (0.17 release) —
Encryption beyond TLS sealed envelopes, Noise, HPKE, E2EE (experimental) — — — — —
JS runtime deps 0 0 6 0 + a peer (Connect) 0 0 (generated)
Polyglot JavaScript only TypeScript only many clients many languages Rust, Go, JS 6 languages

Which wRPC? The Bytecode Alliance's wRPC is an unrelated WIT-based RPC framework for WebAssembly components; webrpc is schema-first code generation over HTTP and JSON. Both are in the table because both are reasonable choices — see Why wRPC?, where every cell has its source.

When NOT to use wRPC

  • You want an API wRPC does not shape. A procedure can declare a real REST mapping — verb, path, status, /vN version paths, schemas, wrpc types --openapi — but the URL surface follows your router. If the HTTP contract itself is the product, design it first.
  • Your stack is polyglot. The protocol is documented and small enough to reimplement, but the only implementation today is JavaScript. gRPC or Connect, webrpc, or — for WebAssembly components — the Bytecode Alliance's wRPC are built for many languages.
  • You need delivery guarantees from rooms. Rooms are at-most-once fan-out, not a queue. Subscriptions with tracked() values and an event log let a client catch up, and the broker family adds at-least-once queue consumers and durable feeds (experimental) — exactly-once is the broker's and your application's to build.
  • You need audio or video. The WebRTC layer moves data over data channels, not media tracks.
  • 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) 83.3 KB 28.0 KB 28.0 KB
@alexify/wrpc — node (client + server) 302.2 KB 101.6 KB —
@alexify/wrpc/ws (WebSocket engine) 32.9 KB 11.6 KB —
@alexify/wrpc/engine (engine port) 33.5 KB 11.8 KB —
@alexify/wrpc/uws (uWebSockets.js adapter) 44.5 KB 16.5 KB —
@alexify/wrpc/fastify 245.8 KB 84.5 KB —
@alexify/wrpc/express 232.3 KB 79.2 KB —
@alexify/wrpc/scaling (rooms backplane) 5.5 KB 2.2 KB —
@alexify/wrpc/broker (broker core, memory broker) 167.2 KB 57.7 KB —
@alexify/wrpc/broker/redis 31.6 KB 11.8 KB —
@alexify/wrpc/broker/nats 30.2 KB 11.2 KB —
@alexify/wrpc/broker/amqp 34.1 KB 12.4 KB —
@alexify/wrpc/broker/kafka 28.6 KB 10.6 KB —
@alexify/wrpc/sse — browser (client transport) 86.6 KB 29.1 KB 30.0 KB
@alexify/wrpc/sse — node 147.3 KB 49.5 KB —
@alexify/wrpc/query (TanStack bindings) 2.7 KB 1.1 KB 2.0 KB
@alexify/wrpc/auth (token strategies) 3.9 KB 1.8 KB 2.0 KB
@alexify/wrpc/deflate (pure-JS DEFLATE codec) 10.7 KB 4.4 KB 5.0 KB
@alexify/wrpc/encryption — browser (primitives, sessions, HPKE, E2EE) 31.5 KB 11.5 KB 12.0 KB
@alexify/wrpc/encryption — node 45.1 KB 16.5 KB —
@alexify/wrpc/webrtc — browser (peer, link, mesh, assertions) 179.6 KB 58.4 KB 59.0 KB
@alexify/wrpc/webrtc — node 193.6 KB 63.0 KB —
@alexify/wrpc/wt (WebTransport server half) 63.6 KB 22.5 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
Message brokers Broker-agnostic capabilities (backplane, log, queue, direct) with adapters for Redis, NATS, RabbitMQ and Kafka, every client injected: durable feeds that resume on any instance, queue consumers into procedures with retry and dead-lettering, and RPC between services over the broker (experimental)
Transports WebSocket, plain HTTP, Server-Sent Events, a worker MessagePort (Service Worker or SharedWorker), WebRTC data channels between browsers (symmetric peers, mesh, built-in or pluggable signaling, stable app-level identity, server-signed trust assertions); WebTransport over HTTP/3 (experimental; WebSocket as the fallback, sessions from an injected host); pluggable wire codec and request metadata (x-wrpc-meta-<key>); compression on every wire, off by default — performance first; deflate, Brotli, zstd or your own codec, negotiated as a preference list
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
Encryption Opt-in, never in place of TLS, for where TLS ends before the data does: sealed backplane, cluster and broker messages under a rotating keyring, a sealed session store, session encryption — canonical Noise on WebSocket/WebTransport, HPKE per request on HTTP/SSE, the server key pinned — and end-to-end helpers. Platform crypto only (AES-256-GCM, X25519, HKDF), checked against the published vectors; other ciphers, KMS keys and post-quantum KEMs by injection
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, buildDictionary, dictionaryCompressor, deflateCompressor, brotliCompressor, zstdCompressor, isCompressor Server · Client · Compression
@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/broker MemoryBroker, brokerFeed, attachConsumers, createPublisher, attachBrokerRpc, isBroker, TopicTails, encodeToken (experimental) Message brokers
@alexify/wrpc/broker/{redis,nats,amqp,kafka} createRedisBroker, createNatsBroker, createAmqpBroker, createKafkaBroker — every client injected (experimental) Redis · NATS · RabbitMQ · Kafka
@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
@alexify/wrpc/deflate createDeflateCodec, inflateRaw, deflateRaw, DeflateError — a pure-JS DEFLATE codec with a preset dictionary, for browsers and anywhere a synchronous codec is wanted Compression
@alexify/wrpc/encryption createEncryption, fetchServerKey, sealedStore, createIdentity, createSealer, createOpener, generateKey, normalizeKeys, aead, x25519, createKdf, createNoise, createHpke, dhKem, isCipher/isDh/isKem/isKeyProvider (experimental) Encryption
@alexify/wrpc/webrtc WrpcPeer, PeerLink, Mesh, PeerHost, RtcLink, wrpcSignaler, createSignalingUnit, createSignalingHooks, createAssertionIssuer, createAssertionVerifier, createW3cAdapter WebRTC, identity and trust
@alexify/wrpc/wt attachSession, acceptSessions, fromFails, failsRequestCallback, fromQuico, WtSocket, isWtSession (experimental; the wt client transport is in the main entry) WebTransport
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

wRPC (Web RPC): a fast, zero-dependency RPC protocol for Node.js and browsers - one router over WebSocket, HTTP, SSE, REST, WebTransport and WebRTC, plus message brokers - typed client, subscriptions, rooms, cluster, binary streams, compression and encryption

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages