WebSockets
On this page
You can use WebSockets for all or some of the communication with your server, see wsLink for how to set it up on the client.
Creating a WebSocket-server
yarn add wsimport { function applyWSSHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): {
broadcastReconnectNotification: () => void;
}
applyWSSHandler } from '@trpc/server/adapters/ws';
import { const WebSocketServer: typeof ServerWebSocketServer } from 'ws';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter } from './routers/app';
import { const createContext: (opts: CreateWSSContextFnOptions) => {}createContext } from './trpc';
const const wss: Server<typeof WebSocket, typeof IncomingMessage>wss = new new WebSocketServer<typeof WebSocket, typeof IncomingMessage>(options?: WebSocket.ServerOptions<typeof WebSocket, typeof IncomingMessage> | undefined, callback?: () => void): Server<typeof WebSocket, typeof IncomingMessage>WebSocketServer({
WebSocket.ServerOptions<U extends typeof WebSocket = typeof WebSocket, V extends typeof IncomingMessage = typeof IncomingMessage>.port?: number | undefinedport: 3001,
});
const const handler: {
broadcastReconnectNotification: () => void;
}
handler = applyWSSHandler<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: WSSHandlerOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): {
broadcastReconnectNotification: () => void;
}
applyWSSHandler({
wss: ws.WebSocketServerwss,
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, IncomingMessage>.router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter,
createContext?: CreateWSSContextFn<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | undefined
createContext,
// Enable heartbeat messages to keep connection open (disabled by default)
keepAlive?: {
enabled: boolean;
pingMs?: number;
pongWaitMs?: number;
} | undefined
keepAlive: {
enabled: booleanenabled: true,
// server ping message interval in milliseconds
pingMs?: number | undefinedpingMs: 30000,
// connection is terminated if pong message is not received in this many milliseconds
pongWaitMs?: number | undefinedpongWaitMs: 5000,
},
});
const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.on(event: "connection", cb: (this: Server<typeof WebSocket, typeof IncomingMessage>, websocket: WebSocket, request: IncomingMessage) => void): Server<typeof WebSocket, typeof IncomingMessage> (+5 overloads)on('connection', (ws: WebSocketws) => {
var console: Consoleconsole.Console.log(...data: any[]): voidlog(`++ Connection (${const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.clients: Set<WebSocket>clients.Set<WebSocket>.size: numbersize})`);
ws: WebSocketws.WebSocket.once(event: "close", listener: (this: WebSocket, code: number, reason: Buffer) => void): WebSocket (+8 overloads)once('close', () => {
var console: Consoleconsole.Console.log(...data: any[]): voidlog(`-- Connection (${const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.clients: Set<WebSocket>clients.Set<WebSocket>.size: numbersize})`);
});
});
var console: Consoleconsole.Console.log(...data: any[]): voidlog('WebSocket Server listening on ws://localhost:3001');
var process: NodeJS.Processprocess.NodeJS.Process.on<"SIGTERM">(eventName: "SIGTERM", listener: (signal: "SIGTERM") => void): NodeJS.Process (+1 overload)on('SIGTERM', () => {
var console: Consoleconsole.Console.log(...data: any[]): voidlog('SIGTERM');
const handler: {
broadcastReconnectNotification: () => void;
}
handler.broadcastReconnectNotification: () => voidbroadcastReconnectNotification();
const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.close(cb?: (err?: Error) => void): voidclose();
});Setting TRPCClient to use WebSockets
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient, function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions<TRouter>): TRPCLink<TRouter>wsLink } from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
// create persistent WebSocket connection
const const wsClient: WsClientwsClient = function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: `ws://localhost:3001`,
});
// configure TRPCClient to use WebSockets transport
const const client: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
client = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
wsLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: WebSocketLinkOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
wsLink({
client: WsClientclient: const wsClient: WsClientwsClient,
}),
],
});Authentication / connection params
In order to authenticate with WebSockets, you can define connectionParams to createWSClient. This will be sent as the first message when the client establishes a WebSocket connection.
import type { type CreateWSSContextFnOptions = {
req: IncomingMessage;
res: ws.WebSocket;
info: TRPCRequestInfo;
}
CreateWSSContextFnOptions } from '@trpc/server/adapters/ws';
export const const createContext: (opts: CreateWSSContextFnOptions) => Promise<{}>createContext = async (opts: CreateWSSContextFnOptionsopts: type CreateWSSContextFnOptions = {
req: IncomingMessage;
res: ws.WebSocket;
info: TRPCRequestInfo;
}
CreateWSSContextFnOptions) => {
const token = opts: CreateWSSContextFnOptionsopts.info: TRPCRequestInfoinfo.TRPCRequestInfo.connectionParams: Dict<string> | nullconnectionParams?.string | undefinedtoken;
// [... authenticate]
return {};
};
export type type Context = {}Context = type Awaited<T> = T extends null | undefined ? T : T extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T
Awaited<type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof const createContext: (opts: CreateWSSContextFnOptions) => Promise<{}>createContext>>;import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient, function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions<TRouter>): TRPCLink<TRouter>wsLink } from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
import class superjsonsuperjson from 'superjson';
const const wsClient: WsClientwsClient = function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: `ws://localhost:3000`,
UrlOptionsWithConnectionParams.connectionParams?: CallbackOrValue<Dict<string> | null> | undefinedconnectionParams: async () => {
return {
token: stringtoken: 'supersecret',
};
},
});
export const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
links: [wsLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: WebSocketLinkOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{}>>>
wsLink({ client: WsClientclient: const wsClient: WsClientwsClient, transformer: DataTransformerOptionstransformer: class superjsonsuperjson })],
});Automatic tracking of id using tracked() (recommended)
If you yield an event using our tracked()-helper and include an id, the client will automatically reconnect when it gets disconnected and send the last known ID when reconnecting as part of the lastEventId-input.
You can send an initial lastEventId when initializing the subscription and it will be automatically updated as the browser receives data.
import class EventEmitter<T extends EventMap<T> = any>
interface EventEmitter<T extends EventMap<T> = any>
EventEmitter, { function on(emitter: EventEmitter, eventName: string | symbol, options?: EventEmitter.OnOptions): NodeJS.AsyncIterator<any[]> (+1 overload)on } from 'events';
import { const initTRPC: TRPCBuilder<object, object>initTRPC, function tracked<TData>(id: string, data: TData): TrackedEnvelope<TData>tracked } from '@trpc/server';
import { import zz } from 'zod';
type type Post = {
id: string;
title: string;
}
Post = { id: stringid: string; title: stringtitle: string };
const const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create();
const const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;
const const router: RouterBuilder<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
router = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
router;
const const ee: EventEmitter<any>ee = new new EventEmitter<any>(options?: EventEmitter.EventEmitterOptions): EventEmitter<any>EventEmitter();
export const const subRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
onPostAdd: SubscriptionProcedure<{
input: {
lastEventId?: string | null | undefined;
} | undefined;
output: AsyncIterable<TrackedData<Post>, void, any>;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
subRouter = const router: RouterBuilder
<{
onPostAdd: SubscriptionProcedure<{
input: {
lastEventId?: string | null | undefined;
} | undefined;
output: AsyncIterable<TrackedData<Post>, void, any>;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>(_: {
onPostAdd: SubscriptionProcedure<{
input: {
lastEventId?: string | null | undefined;
} | undefined;
output: AsyncIterable<TrackedData<Post>, void, any>;
meta: object;
errorShape: DefaultErrorShape;
}>;
}) => BuiltRouter<...>
router({
onPostAdd: SubscriptionProcedure<{
input: {
lastEventId?: string | null | undefined;
} | undefined;
output: AsyncIterable<TrackedData<Post>, void, any>;
meta: object;
errorShape: DefaultErrorShape;
}>
onPostAdd: const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodOptional<z.ZodObject<{
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>>>(schema: z.ZodOptional<z.ZodObject<{
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>>): ProcedureBuilder<object, object, object, {
lastEventId?: string | null | undefined;
} | undefined, {
lastEventId?: string | null | undefined;
} | undefined, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(
import zz
.function object<{
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}>(shape?: {
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>
object({
// lastEventId is the last event id that the client has received
// On the first call, it will be whatever was passed in the initial setup
// If the client reconnects, it will be the last event id that the client received
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>lastEventId: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodType<any, any, $ZodStringInternals<string>>.nullish(): z.ZodOptional<z.ZodNullable<z.ZodString>>nullish(),
})
.ZodType<any, any, $ZodObjectInternals<{ lastEventId: ZodOptional<ZodNullable<ZodString>>; }, $strip>>.optional(): z.ZodOptional<z.ZodObject<{
lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>>
optional(),
)
.ProcedureBuilder<object, object, object, { lastEventId?: string | null | undefined; } | undefined, { lastEventId?: string | null | undefined; } | undefined, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.subscription<AsyncGenerator<TrackedEnvelope<Post>, void, any>>(resolver: ProcedureResolver<object, object, object, {
lastEventId?: string | null | undefined;
} | undefined, UnsetMarker, AsyncGenerator<TrackedEnvelope<Post>, void, any>>): SubscriptionProcedure<{
input: {
lastEventId?: string | null | undefined;
} | undefined;
output: AsyncIterable<TrackedData<Post>, void, any>;
meta: object;
errorShape: DefaultErrorShape;
}> (+1 overload)
subscription(async function* (opts: ProcedureResolverOptions<object, object, object, {
lastEventId?: string | null | undefined;
} | undefined>
opts) {
if (opts: ProcedureResolverOptions<object, object, object, {
lastEventId?: string | null | undefined;
} | undefined>
opts.ProcedureResolverOptions<object, object, object, { lastEventId?: string | null | undefined; } | undefined>.input: {
lastEventId?: string | null | undefined;
} | undefined
input?.lastEventId?: string | null | undefinedlastEventId) {
// [...] get the posts since the last event id and yield them
}
// listen for new events
for await (const [const data: anydata] of function on(emitter: EventEmitter, eventName: string | symbol, options?: EventEmitter.OnOptions): NodeJS.AsyncIterator<any[]> (+1 overload)on(const ee: EventEmitter<any>ee, 'add', {
// Passing the AbortSignal from the request automatically cancels the event emitter when the subscription is aborted
EventEmitter<T extends EventMap<T> = any>.Abortable.signal?: AbortSignal | undefinedsignal: opts: ProcedureResolverOptions<object, object, object, {
lastEventId?: string | null | undefined;
} | undefined>
opts.ProcedureResolverOptions<TContext, _TMeta, TContextOverridesIn, TInputOut>.signal: AbortSignal | undefinedsignal,
})) {
const const post: Postpost = const data: anydata as type Post = {
id: string;
title: string;
}
Post;
// tracking the post id ensures the client can reconnect at any time and get the latest events since this id
yield tracked<Post>(id: string, data: Post): TrackedEnvelope<Post>tracked(const post: Postpost.id: stringid, const post: Postpost);
}
}),
});WebSockets RPC Specification
You can read more details by drilling into the TypeScript definitions:
query / mutation
Request
interface RequestMessage {
RequestMessage.id: string | numberid: number | string;
RequestMessage.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
RequestMessage.method: "query" | "mutation"method: 'query' | 'mutation';
RequestMessage.params: {
path: string;
input?: unknown;
}
params: {
path: stringpath: string;
input?: unknowninput?: unknown; // <-- pass input of procedure, serialized by transformer
};
}Response
... below, or an error.
interface ResponseMessage {
ResponseMessage.id: string | numberid: number | string;
ResponseMessage.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
ResponseMessage.result: {
type: "data";
data: TOutput;
}
result: {
type: "data"type: 'data'; // always 'data' for mutation / queries
data: anydata: type TOutput = anyTOutput; // output from procedure
};
}subscription / subscription.stop
Start a subscription
interface SubscriptionRequest {
SubscriptionRequest.id: string | numberid: number | string;
SubscriptionRequest.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
SubscriptionRequest.method: "subscription"method: 'subscription';
SubscriptionRequest.params: {
path: string;
input?: unknown;
}
params: {
path: stringpath: string;
input?: unknowninput?: unknown; // <-- pass input of procedure, serialized by transformer
};
}To cancel a subscription, call subscription.stop
interface SubscriptionStopRequest {
SubscriptionStopRequest.id: string | numberid: number | string; // <-- id of your created subscription
SubscriptionStopRequest.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
SubscriptionStopRequest.method: "subscription.stop"method: 'subscription.stop';
}Subscription response shape
... below, or an error.
interface SubscriptionResponse {
SubscriptionResponse.id: string | numberid: number | string;
SubscriptionResponse.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
SubscriptionResponse.result: {
type: "data";
data: TData;
} | {
type: "started";
} | {
type: "stopped";
}
result:
| {
type: "data"type: 'data';
data: anydata: type TData = anyTData; // subscription emitted data
}
| {
type: "started"type: 'started'; // subscription started
}
| {
type: "stopped"type: 'stopped'; // subscription stopped
};
}Connection params
If the connection is initialized with ?connectionParams=1, the first message has to be connection params.
interface ConnectionParamsMessage {
ConnectionParamsMessage.data: Record<string, string> | nulldata: type Record<K extends keyof any, T> = { [P in K]: T; }Record<string, string> | null;
ConnectionParamsMessage.method: "connectionParams"method: 'connectionParams';
}Errors
See https://www.jsonrpc.org/specification#error_object or Error Formatting.
Notifications from Server to Client
{ id: null, type: 'reconnect' }
Tells clients to reconnect before shutting down the server. Invoked by wssHandler.broadcastReconnectNotification().