npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@webrtc-node/trpc-webrtc-link

v0.2.0

Published

A complete tRPC v11 client link and server transport for WebRTC RTCDataChannels.

Readme

@webrtc-node/trpc-webrtc-link

A tRPC v11 terminating client link and server handler for ordered, reliable WebRTC RTCDataChannel connections.

The package transports concurrent queries, mutations, and subscriptions. It supports cancellation, transformers, custom error formatting, tracked() events, reconnect and subscription resumption, connection state, connection parameters, keep-alive, frame limits, and fair backpressure.

SDP/ICE signaling remains application-owned: the package consumes data channels but does not create peer connections or decide who a peer is.

Requirements

  • Node.js 20.19 or newer;
  • TypeScript 5.7.2 or newer;
  • matching @trpc/client and @trpc/server versions in the supported range >=11.17.0 <11.19.0;
  • an ordered, reliable RTCDataChannel.

Install the transport and tRPC peers:

npm install @webrtc-node/trpc-webrtc-link @trpc/client @trpc/server

Node.js peers also need the current WebRTC implementation:

npm install @webrtc-node/webrtc

The transport never imports or bundles @webrtc-node/webrtc; browsers use their native WebRTC APIs.

Basic client

An existing channel negotiates immediately, even before the first tRPC operation. Await connect() when application startup depends on readiness.

import { createTRPCClient } from '@trpc/client';
import { createWebRTCLink } from '@webrtc-node/trpc-webrtc-link';
import type { AppRouter } from './router';

const link = createWebRTCLink<AppRouter>({
  channel,
  connectionParams: {
    authorization: `Bearer ${accessToken}`,
  },
  keepAlive: {
    enabled: true,
    intervalMs: 5_000,
    pongTimeoutMs: 1_000,
  },
});

const client = createTRPCClient<AppRouter>({
  links: [link],
});

await link.connect();

const greeting = await client.greeting.query();
const count = await client.counter.increment.mutate();

const subscription = client.events.subscribe(undefined, {
  onData(event) {
    console.log(event);
  },
  onConnectionStateChange(state) {
    console.log(state.state); // idle, connecting, or pending
  },
});

subscription.unsubscribe();
link.close();

If the router has a transformer, provide the matching transformer to the link:

const link = createWebRTCLink<AppRouter>({
  channel,
  transformer: superjson,
});

The link is a standard tRPC terminating link. It composes with loggerLink, retryLink, and splitLink, and works with vanilla tRPC, TanStack Query, and the tRPC React integrations without package-specific bindings.

Server handler

Create one handler for every server-side data channel. Context is created once per channel and receives typed peer metadata, connection parameters, and a signal that aborts when the channel or handler closes.

import { createWebRTCHandler } from '@webrtc-node/trpc-webrtc-link';
import { TRPCError } from '@trpc/server';

const handler = createWebRTCHandler({
  router: appRouter,
  channel,
  peer: {
    userId: authenticatedPeer.userId,
    peerConnection,
  },
  createContext({ peer, connectionParams, signal }) {
    if (connectionParams?.authorization !== expectedAuthorization) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }
    return {
      userId: peer.userId,
      signal,
    };
  },
  keepAlive: {
    enabled: true,
    intervalMs: 30_000,
    pongTimeoutMs: 5_000,
  },
  maxConcurrentOperations: 100,
  onError({ error, path }) {
    console.error(path, error);
  },
});

await handler.ready;

// Before rotating a peer connection or server:
await handler.requestReconnect('server rotation');

// During final cleanup:
handler.close({ closeChannel: true });

close() aborts procedures and subscription iterators, rejects queued writes, and removes all listeners. The channel is only closed when closeChannel: true is supplied.

Signaling and reconnect

Use a channel factory when signaling can establish replacement peer connections. The factory is called once per attempt and must return a fresh channel. The remote application must attach a new createWebRTCHandler() to the matching server channel.

const link = createWebRTCLink<AppRouter>({
  channel: async ({ signal, attempt, cause }) => {
    console.log({ attempt, cause });
    return signaling.createDataChannel({ signal });
  },
  reconnect: {
    enabled: true,
    maxAttempts: 8,
    retryDelayMs: (attempt) => (attempt === 0 ? 0 : Math.min(1_000 * 2 ** attempt, 30_000)),
    shouldRetry: ({ error }) => !isPermanentSignalingError(error),
  },
  connectionParams: () => ({
    authorization: `Bearer ${getCurrentAccessToken()}`,
  }),
});

Connection parameters are evaluated again for each attempt. The factory's signal is aborted when an attempt times out, is replaced, or the link closes.

With automatic reconnect enabled:

  • active subscriptions remain registered;
  • their last delivered tracked event ID is sent to the new server;
  • queries and mutations interrupted after transmission fail, allowing retryLink or the consuming framework to decide whether replay is safe;
  • subscription observers receive tRPC connection states.

Automatic reconnect requires a factory. Without automatic reconnect, the same factory is still reusable by retryLink or a later operation after a channel failure.

Set lazy: true to delay a factory until connect() or the first operation. Direct channels always negotiate immediately so a server handshake timeout cannot close an otherwise idle connection.

The extended link exposes:

await link.connect();
await link.reconnect();

console.log(link.connectionState);

const unsubscribe = link.subscribeConnectionState((state) => {
  console.log(state);
});

unsubscribe();
link.close();

Tracked subscriptions

The transport implements tRPC's complete tracked() contract. Clients receive the inferred { id, data } value, while the result-level ID is retained for retryLink and channel replacement.

import { initTRPC, tracked } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

const router = t.router({
  events: t.procedure
    .input(
      z
        .object({
          lastEventId: z.string().nullish(),
        })
        .optional(),
    )
    .subscription(async function* ({ input, signal }) {
      for await (const event of readEventsAfter(input?.lastEventId, signal)) {
        yield tracked(event.id, event);
      }
    }),
});
client.events.subscribe(undefined, {
  onData(event) {
    console.log(event.id, event.data);
  },
});

For operation-level errors, place tRPC's retryLink before this terminating link. retryLink automatically injects the last known event ID into the next subscription input.

Cancellation and delivery

  • Query and mutation AbortSignal cancellation sends a cancel frame.
  • Subscription unsubscribe() sends a cancel frame.
  • Every server procedure receives an operation-specific signal.
  • Closing a channel aborts all server operations.

Cancellation is best effort if the channel disappears before the frame is delivered. Mutations are not automatically replayed, and the package does not claim exactly-once execution.

Backpressure and limits

Both endpoints use RTCDataChannel.bufferedAmount. Writes pause above the high watermark, resume after bufferedamountlow, and drain operation queues round-robin so a busy subscription cannot starve other calls.

{
  backpressure: {
    highWatermark: 1024 * 1024,
    lowWatermark: 256 * 1024,
    queueLimit: 1024,
    maxMessageBytes: 1024 * 1024,
  },
}

Frames are never silently dropped. Queue overflow fails the affected operation with WebRTCQueueOverflowError. Configure maxMessageBytes at or below the effective SCTP message size used by both peers. Servers can additionally set maxConcurrentOperations.

Errors and lifecycle callbacks

Remote tRPC errors and transport failures surface as TRPCClientError. Transport failures include:

error.meta?.transport === 'webrtc';
error.meta?.transportCode;

Transport codes distinguish channel closure, failed opening, handshake timeout, keep-alive timeout, protocol errors, queue overflow, exhausted reconnect attempts, and unreliable channels.

Client options include onOpen, onClose, onError, onConnectionStateChange, and onProtocolError. Server options include onError and onProtocolError. Exceptions thrown by lifecycle callbacks do not break transport event processing.

Protocol and security

The wire identifier is trpc-webrtc/1. It uses validated JSON text frames over an ordered, reliable SCTP stream. See the protocol reference for frame details.

WebRTC encrypts transport traffic, but applications must still:

  • authenticate and authorize signaling participants;
  • protect SDP and ICE exchange against peer substitution;
  • treat peer metadata as trusted only when signaling established it;
  • validate every procedure input;
  • use short-lived connection parameters where appropriate;
  • set queue, message, and concurrency limits for untrusted peers.

Deliberate non-goals

The package does not provide:

  • SDP/ICE signaling or peer discovery;
  • a specific authentication system;
  • request batching (operations are already concurrently multiplexed);
  • binary codecs;
  • unrelated-protocol multiplexing;
  • exactly-once delivery.

Support