Skip to content
tRPC

Subscriptions

On this page

Introduction

Subscriptions are a type of real-time event stream between the client and server. Use subscriptions when you need to push real-time updates to the client.

With tRPC's subscriptions, the client establishes and maintains a persistent connection to the server plus automatically attempts to reconnect and recover gracefully if disconnected with the help of tracked() events.

WebSockets or Server-sent Events?

You can either use WebSockets or Server-sent Events (SSE) to setup real-time subscriptions in tRPC.

If you are unsure which one to use, we recommend using SSE for subscriptions as it's easier to setup and doesn't require setting up a WebSocket server.

Reference projects

TypeExample TypeLink
WebSocketsBare-minimum Node.js WebSockets example/examples/standalone-server
SSEFull-stack SSE implementationgithub.com/trpc/examples-next-sse-chat
WebSocketsFull-stack WebSockets implementationgithub.com/trpc/examples-next-prisma-websockets-starter

Basic example

server.ts
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 'node:events';
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server'; 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
();
type
type Post = {
    id: string;
    title: string;
}
Post
= { id: stringid: string; title: stringtitle: string };
const const ee: EventEmitter<any>ee = new new EventEmitter<any>(options?: EventEmitter.EventEmitterOptions): EventEmitter<any>EventEmitter(); export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
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
<{
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
onPostAdd: SubscriptionProcedure<{
    input: void;
    output: AsyncIterable<Post, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}>
onPostAdd
:
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.
ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.subscription<AsyncGenerator<Post, void, any>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<Post, void, any>>): SubscriptionProcedure<{
    input: void;
    output: AsyncIterable<Post, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}> (+1 overload)
subscription
(async function* (opts: ProcedureResolverOptions<object, object, object, UnsetMarker>opts) {
// 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 request is aborted EventEmitter<T extends EventMap<T> = any>.Abortable.signal?: AbortSignal | undefinedsignal: opts: ProcedureResolverOptions<object, object, object, UnsetMarker>opts.ProcedureResolverOptions<object, object, object, UnsetMarker>.signal: AbortSignal | undefinedsignal, })) { const const post: Postpost = const data: anydata as
type Post = {
    id: string;
    title: string;
}
Post
;
yield const post: Postpost; } }), });

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.

You can send an initial lastEventId when initializing the subscription and it will be automatically updated as the browser receives data.

  • For SSE, this is part of the EventSource-spec and will be propagated through lastEventId in your .input().
  • For WebSockets, our wsLink will automatically send the last known ID and update it 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 'node: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'; class class IterableEventEmitterIterableEventEmitter extends class EventEmitter<T extends EventMap<T> = any>EventEmitter {
IterableEventEmitter.toIterable(eventName: string, opts?: {
    signal?: AbortSignal;
}): NodeJS.AsyncIterator<any[], undefined, any>
toIterable
(eventName: stringeventName: string,
opts: {
    signal?: AbortSignal;
} | undefined
opts
?: { signal?: AbortSignal | undefinedsignal?: AbortSignal }) {
return function on(emitter: EventEmitter, eventName: string | symbol, options?: EventEmitter.OnOptions): NodeJS.AsyncIterator<any[]> (+1 overload)on(this, eventName: stringeventName,
opts: {
    signal?: AbortSignal;
} | undefined
opts
);
} } 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: IterableEventEmitteree = new constructor IterableEventEmitter<any>(options?: EventEmitter.EventEmitterOptions): IterableEventEmitterIterableEventEmitter(); 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
) {
// We start by subscribing to the ee so that we don't miss any new events while fetching const const iterable: NodeJS.AsyncIterator<any[], undefined, any>iterable = const ee: IterableEventEmitteree.
IterableEventEmitter.toIterable(eventName: string, opts?: {
    signal?: AbortSignal;
}): NodeJS.AsyncIterator<any[], undefined, any>
toIterable
('add', {
// Passing the AbortSignal from the request automatically cancels the event emitter when the request is aborted signal?: AbortSignal | undefinedsignal:
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: string | null | undefined;
} | undefined>
opts
.ProcedureResolverOptions<TContext, _TMeta, TContextOverridesIn, TInputOut>.signal: AbortSignal | undefinedsignal,
}); 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 // const items = await db.post.findMany({ ... }) // for (const item of items) { // yield tracked(item.id, item); // } } // listen for new events from the iterable we set up above for await (const [const data: anydata] of const iterable: NodeJS.AsyncIterator<any[], undefined, any>iterable) { 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); } }), });

Pull data in a loop

This recipe is useful when you want to periodically check for new data from a source like a database and push it to the client.

server.ts
import { function tracked<TData>(id: string, data: TData): TrackedEnvelope<TData>tracked } from '@trpc/server';
import { import zz } from 'zod';
import { const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure, 
const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
} from './trpc';
export const
const subRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<TrackedData<Post>, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
subRouter
=
router<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<TrackedData<Post>, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<TrackedData<Post>, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>
router
({
onPostAdd: SubscriptionProcedure<{
    input: {
        lastEventId?: unknown;
    };
    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.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
}, z.core.$strip>>(schema: z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    lastEventId?: unknown;
}, {
    lastEventId?: Date | null | undefined;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(
import zz.
function object<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
}>(shape?: {
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
}, 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 // The id is the createdAt of the post lastEventId: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>lastEventId: import zz.import coercecoerce.function date<unknown>(params?: string | z.core.$ZodDateParams): z.ZodCoercedDate<unknown>date().ZodType<any, any, $ZodDateInternals<unknown>>.nullish(): z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>nullish(), }), ) .
ProcedureBuilder<object, object, object, { lastEventId?: unknown; }, { lastEventId?: Date | null | undefined; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.subscription<AsyncGenerator<TrackedEnvelope<Post>, void, any>>(resolver: ProcedureResolver<object, object, object, {
    lastEventId?: Date | null | undefined;
}, UnsetMarker, AsyncGenerator<TrackedEnvelope<Post>, void, any>>): SubscriptionProcedure<{
    input: {
        lastEventId?: unknown;
    };
    output: AsyncIterable<TrackedData<Post>, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}> (+1 overload)
subscription
(async function* (
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: Date | null | undefined;
}>
opts
) {
// `opts.signal` is an AbortSignal that will be aborted when the client disconnects. let let lastEventId: Date | nulllastEventId =
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: Date | null | undefined;
}>
opts
.
ProcedureResolverOptions<object, object, object, { lastEventId?: Date | null | undefined; }>.input: {
    lastEventId?: Date | null | undefined;
}
input
?.lastEventId?: Date | null | undefinedlastEventId ?? null;
// We use a `while` loop that checks `!opts.signal.aborted` while (!
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: Date | null | undefined;
}>
opts
.ProcedureResolverOptions<TContext, _TMeta, TContextOverridesIn, TInputOut>.signal: AbortSignal | undefinedsignal!.AbortSignal.aborted: booleanaborted) {
const const posts: Post[]posts = await
const db: {
    post: {
        findMany(opts: {
            where?: {
                createdAt?: {
                    gt: Date;
                };
            };
            orderBy?: {
                createdAt: string;
            };
        }): Promise<Post[]>;
    };
}
db
.
post: {
    findMany(opts: {
        where?: {
            createdAt?: {
                gt: Date;
            };
        };
        orderBy?: {
            createdAt: string;
        };
    }): Promise<Post[]>;
}
post
.
function findMany(opts: {
    where?: {
        createdAt?: {
            gt: Date;
        };
    };
    orderBy?: {
        createdAt: string;
    };
}): Promise<Post[]>
findMany
({
// If we have a `lastEventId`, we only fetch posts created after it.
where?: {
    createdAt?: {
        gt: Date;
    };
} | undefined
where
: let lastEventId: Date | nulllastEventId
? {
createdAt?: {
    gt: Date;
} | undefined
createdAt
: {
gt: Dategt: let lastEventId: DatelastEventId, }, } : var undefinedundefined,
orderBy?: {
    createdAt: string;
} | undefined
orderBy
: {
createdAt: stringcreatedAt: 'asc', }, }); for (const const post: Postpost of const posts: Post[]posts) { // `tracked` is a helper that sends an `id` with each event. // This allows the client to resume from the last received event upon reconnection. yield tracked<Post>(id: string, data: Post): TrackedEnvelope<Post>tracked(const post: Postpost.createdAt: DatecreatedAt.Date.toJSON(key?: any): stringtoJSON(), const post: Postpost); let lastEventId: Date | nulllastEventId = const post: Postpost.createdAt: DatecreatedAt; } // Wait for a bit before polling again to avoid hammering the database. await function sleep(ms: number): Promise<void>sleep(1_000); } }), });

Stopping a subscription from the server

If you need to stop a subscription from the server, simply return in the generator function.

export const 
const subRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<never, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
subRouter
=
const router: RouterBuilder
<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<never, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<never, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<never, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
onPostAdd: SubscriptionProcedure<{
    input: {
        lastEventId?: unknown;
    };
    output: AsyncIterable<never, 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.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}, z.core.$strip>>(schema: z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    lastEventId?: unknown;
}, {
    lastEventId?: number | undefined;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(
import zz.
function object<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}>(shape?: {
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}, z.core.$strip>
object
({
lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>lastEventId: import zz.import coercecoerce.function number<unknown>(params?: string | z.core.$ZodNumberParams): z.ZodCoercedNumber<unknown>number()._ZodNumber<$ZodNumberInternals<unknown>>.min(value: number, params?: string | z.core.$ZodCheckGreaterThanParams): z.ZodCoercedNumber<unknown>min(0).ZodType<any, any, $ZodNumberInternals<unknown>>.optional(): z.ZodOptional<z.ZodCoercedNumber<unknown>>optional(), }), ) .
ProcedureBuilder<object, object, object, { lastEventId?: unknown; }, { lastEventId?: number | undefined; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.subscription<AsyncGenerator<never, void, any>>(resolver: ProcedureResolver<object, object, object, {
    lastEventId?: number | undefined;
}, UnsetMarker, AsyncGenerator<never, void, any>>): SubscriptionProcedure<{
    input: {
        lastEventId?: unknown;
    };
    output: AsyncIterable<never, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}> (+1 overload)
subscription
(async function* (
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: number | undefined;
}>
opts
) {
let let index: numberindex =
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: number | undefined;
}>
opts
.
ProcedureResolverOptions<object, object, object, { lastEventId?: number | undefined; }>.input: {
    lastEventId?: number | undefined;
}
input
.lastEventId?: number | undefinedlastEventId ?? 0;
while (!
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: number | undefined;
}>
opts
.ProcedureResolverOptions<TContext, _TMeta, TContextOverridesIn, TInputOut>.signal: AbortSignal | undefinedsignal!.AbortSignal.aborted: booleanaborted) {
const const idx: numberidx = let index: numberindex++; if (const idx: numberidx > 100) { // With this, the subscription will stop and the client will disconnect return; } await new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
Promise
((resolve: (value: unknown) => voidresolve) => function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): numbersetTimeout(resolve: (value: unknown) => voidresolve, 10));
} }), });

On the client, you just .unsubscribe() the subscription.

Cleanup of side effects

If you need to clean up any side-effects of your subscription you can use the try...finally pattern, as trpc invokes the .return() of the Generator Instance when the subscription stops for any reason.

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 } from '@trpc/server'; 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: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
subRouter
=
const router: RouterBuilder
<{
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: void;
        output: AsyncIterable<Post, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
onPostAdd: SubscriptionProcedure<{
    input: void;
    output: AsyncIterable<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>.subscription<AsyncGenerator<Post, void, any>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<Post, void, any>>): SubscriptionProcedure<{
    input: void;
    output: AsyncIterable<Post, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}> (+1 overload)
subscription
(async function* (opts: ProcedureResolverOptions<object, object, object, UnsetMarker>opts) {
let let timeout: NodeJS.Timeout | undefinedtimeout: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number (+1 overload)setTimeout> | undefined; try { 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', { EventEmitter<T extends EventMap<T> = any>.Abortable.signal?: AbortSignal | undefinedsignal: opts: ProcedureResolverOptions<object, object, object, UnsetMarker>opts.ProcedureResolverOptions<object, object, object, UnsetMarker>.signal: AbortSignal | undefinedsignal, })) { let timeout: NodeJS.Timeout | undefinedtimeout = function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)setTimeout(() => var console: Consoleconsole.Console.log(...data: any[]): voidlog('Pretend like this is useful')); const const post: Postpost = const data: anydata as
type Post = {
    id: string;
    title: string;
}
Post
;
yield const post: Postpost; } } finally { if (let timeout: NodeJS.Timeout | undefinedtimeout) function clearTimeout(timeout: NodeJS.Timeout | string | number | undefined): void (+1 overload)clearTimeout(let timeout: NodeJS.Timeouttimeout); } }), });

Error handling

Throwing an error in a generator function propagates to trpc's onError() on the backend.

If the error thrown is a 5xx error, the client will automatically attempt to reconnect based on the last event id that is tracked using tracked(). For other errors, the subscription will be cancelled and propagate to the onError() callback.

Output validation

Since subscriptions are async iterators, you have to go through the iterator to validate the output.

Example with Zod v4

zAsyncIterable.ts
import type { type TrackedEnvelope<TData> = [TrackedId, TData, unique symbol]TrackedEnvelope } from '@trpc/server';
import { function isTrackedEnvelope<TData>(value: unknown): value is TrackedEnvelope<TData>isTrackedEnvelope, function tracked<TData>(id: string, data: TData): TrackedEnvelope<TData>tracked } from '@trpc/server';
import { import zz } from 'zod';

function function isAsyncIterable<TValue, TReturn = unknown>(value: unknown): value is AsyncIterable<TValue, TReturn>isAsyncIterable<function (type parameter) TValue in isAsyncIterable<TValue, TReturn = unknown>(value: unknown): value is AsyncIterable<TValue, TReturn>TValue, function (type parameter) TReturn in isAsyncIterable<TValue, TReturn = unknown>(value: unknown): value is AsyncIterable<TValue, TReturn>TReturn = unknown>(
  value: unknownvalue: unknown,
): value: unknownvalue is interface AsyncIterable<T, TReturn = any, TNext = any>AsyncIterable<function (type parameter) TValue in isAsyncIterable<TValue, TReturn = unknown>(value: unknown): value is AsyncIterable<TValue, TReturn>TValue, function (type parameter) TReturn in isAsyncIterable<TValue, TReturn = unknown>(value: unknown): value is AsyncIterable<TValue, TReturn>TReturn> {
  return !!value: unknownvalue && typeof value: {}value === 'object' && var Symbol: SymbolConstructorSymbol.SymbolConstructor.asyncIterator: typeof Symbol.asyncIteratorasyncIterator in value: objectvalue;
}
const const trackedEnvelopeSchema: z.ZodCustom<TrackedEnvelope<unknown>, TrackedEnvelope<unknown>>trackedEnvelopeSchema =
  import zz.
function custom<TrackedEnvelope<unknown>>(fn?: (data: unknown) => unknown, _params?: string | {
    abort?: boolean | undefined | undefined;
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    path?: PropertyKey[] | undefined | undefined;
    params?: Record<string, any> | undefined;
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssue>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodCustom<TrackedEnvelope<unknown>, TrackedEnvelope<unknown>>
custom
<type TrackedEnvelope<TData> = [TrackedId, TData, unique symbol]TrackedEnvelope<unknown>>(function isTrackedEnvelope<TData>(value: unknown): value is TrackedEnvelope<TData>isTrackedEnvelope);
/** * A Zod schema helper designed specifically for validating async iterables. This schema ensures that: * 1. The value being validated is an async iterable. * 2. Each item yielded by the async iterable conforms to a specified type. * 3. The return value of the async iterable, if any, also conforms to a specified type. */ export function
function zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
zAsyncIterable
<
function (type parameter) TYieldIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldIn
,
function (type parameter) TYieldOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldOut
,
function (type parameter) TReturnIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnIn
= void,
function (type parameter) TReturnOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnOut
= void,
function (type parameter) Tracked in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
Tracked
extends boolean = false,
>(
opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}
opts
: {
/** * Validate the value yielded by the async generator */ yield: z.ZodType<TYieldOut, TYieldIn, z.core.$ZodTypeInternals<TYieldOut, TYieldIn>>yield: import zz.interface ZodType<out Output = unknown, out Input = unknown, out Internals extends z.core.$ZodTypeInternals<Output, Input> = z.core.$ZodTypeInternals<Output, Input>>ZodType<
function (type parameter) TYieldOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldOut
,
function (type parameter) TYieldIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldIn
>;
/** * Validate the return value of the async generator * @remarks not applicable for subscriptions */ return?: z.ZodType<TReturnOut, TReturnIn, z.core.$ZodTypeInternals<TReturnOut, TReturnIn>> | undefinedreturn?: import zz.interface ZodType<out Output = unknown, out Input = unknown, out Internals extends z.core.$ZodTypeInternals<Output, Input> = z.core.$ZodTypeInternals<Output, Input>>ZodType<
function (type parameter) TReturnOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnOut
,
function (type parameter) TReturnIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnIn
>;
/** * Whether the yielded values are tracked * @remarks only applicable for subscriptions */ tracked?: Tracked | undefinedtracked?:
function (type parameter) Tracked in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
Tracked
;
}) { return import zz .
function custom<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>>(fn?: (data: unknown) => unknown, _params?: string | {
    abort?: boolean | undefined | undefined;
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    path?: PropertyKey[] | undefined | undefined;
    params?: Record<string, any> | undefined;
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssue>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodCustom<...>
custom
<
interface AsyncIterable<T, TReturn = any, TNext = any>AsyncIterable<
function (type parameter) Tracked in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
Tracked
extends true ? type TrackedEnvelope<TData> = [TrackedId, TData, unique symbol]TrackedEnvelope<
function (type parameter) TYieldIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldIn
> :
function (type parameter) TYieldIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldIn
,
function (type parameter) TReturnIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnIn
> >((val: unknownval) => function isAsyncIterable<unknown, unknown>(value: unknown): value is AsyncIterable<unknown, unknown>isAsyncIterable(val: unknownval)) .ZodType<any, any, $ZodCustomInternals<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>, AsyncIterable<...>>>.transform<AsyncGenerator<Awaited<TYieldOut> | TrackedEnvelope<Awaited<TYieldOut>>, Awaited<TReturnOut> | undefined, unknown>>(transform: (arg: AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>, ctx: z.core.$RefinementCtx<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>>) => AsyncGenerator<...> | Promise<...>): z.ZodPipe<...>transform(async function* (iter: AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>iter) { const const iterator: AsyncIterator<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, any>iterator = iter: AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>iter[var Symbol: SymbolConstructorSymbol.SymbolConstructor.asyncIterator: typeof Symbol.asyncIteratorasyncIterator](); try { let let next: anynext; while ((let next: anynext = await const iterator: AsyncIterator<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, any>iterator.AsyncIterator<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, any>.next(...[value]: [] | [any]): Promise<IteratorResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>>next()) && !let next: IteratorResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>next.done?: boolean | undefineddone) { if (
opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}
opts
.tracked?: Tracked | undefinedtracked) {
const [const id: TrackedIdid, const data: unknowndata] = const trackedEnvelopeSchema: z.ZodCustom<TrackedEnvelope<unknown>, TrackedEnvelope<unknown>>trackedEnvelopeSchema.ZodType<any, any, $ZodCustomInternals<TrackedEnvelope<unknown>, TrackedEnvelope<unknown>>>.parse(data: unknown, params?: z.core.ParseContext<z.core.$ZodIssue>): TrackedEnvelope<unknown>parse(let next: IteratorYieldResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn>next.IteratorYieldResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn>.value: Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldInvalue); yield tracked<Awaited<TYieldOut>>(id: string, data: Awaited<TYieldOut>): TrackedEnvelope<Awaited<TYieldOut>>tracked(const id: TrackedIdid, await
opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}
opts
.yield: z.ZodType<TYieldOut, TYieldIn, z.core.$ZodTypeInternals<TYieldOut, TYieldIn>>yield.ZodType<TYieldOut, TYieldIn, $ZodTypeInternals<TYieldOut, TYieldIn>>.parseAsync(data: unknown, params?: z.core.ParseContext<z.core.$ZodIssue>): Promise<TYieldOut>parseAsync(const data: unknowndata));
continue; } yield
opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}
opts
.yield: z.ZodType<TYieldOut, TYieldIn, z.core.$ZodTypeInternals<TYieldOut, TYieldIn>>yield.ZodType<TYieldOut, TYieldIn, $ZodTypeInternals<TYieldOut, TYieldIn>>.parseAsync(data: unknown, params?: z.core.ParseContext<z.core.$ZodIssue>): Promise<TYieldOut>parseAsync(let next: IteratorYieldResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn>next.IteratorYieldResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn>.value: Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldInvalue);
} if (
opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}
opts
.return?: z.ZodType<TReturnOut, TReturnIn, z.core.$ZodTypeInternals<TReturnOut, TReturnIn>> | undefinedreturn) {
return await
opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}
opts
.return?: z.ZodType<TReturnOut, TReturnIn, z.core.$ZodTypeInternals<TReturnOut, TReturnIn>>return.ZodType<TReturnOut, TReturnIn, $ZodTypeInternals<TReturnOut, TReturnIn>>.parseAsync(data: unknown, params?: z.core.ParseContext<z.core.$ZodIssue>): Promise<TReturnOut>parseAsync(let next: IteratorReturnResult<TReturnIn>next.IteratorReturnResult<TReturnIn>.value: TReturnIn = voidvalue);
} return; } finally { await const iterator: AsyncIterator<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, any>iterator.AsyncIterator<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, any>.return?(value?: TReturnIn | PromiseLike<TReturnIn> | undefined): Promise<IteratorResult<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn>>return?.(); } }) as import zz.interface ZodType<out Output = unknown, out Input = unknown, out Internals extends z.core.$ZodTypeInternals<Output, Input> = z.core.$ZodTypeInternals<Output, Input>>ZodType< interface AsyncIterable<T, TReturn = any, TNext = any>AsyncIterable<
function (type parameter) Tracked in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
Tracked
extends true ? type TrackedEnvelope<TData> = [TrackedId, TData, unique symbol]TrackedEnvelope<
function (type parameter) TYieldIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldIn
> :
function (type parameter) TYieldIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldIn
,
function (type parameter) TReturnIn in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnIn
,
unknown >, interface AsyncIterable<T, TReturn = any, TNext = any>AsyncIterable<
function (type parameter) Tracked in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
Tracked
extends true ? type TrackedEnvelope<TData> = [TrackedId, TData, unique symbol]TrackedEnvelope<
function (type parameter) TYieldOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldOut
> :
function (type parameter) TYieldOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TYieldOut
,
function (type parameter) TReturnOut in zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
TReturnOut
,
unknown > >; }

Now you can use this helper to validate the output of your subscription procedures:

_app.ts
import { function tracked<TData>(id: string, data: TData): TrackedEnvelope<TData>tracked } from '@trpc/server';
import { import zz } from 'zod';
import { const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure, 
const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
} from './trpc';
import {
function zAsyncIterable<TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false>(opts: {
    yield: z.ZodType<TYieldOut, TYieldIn>;
    return?: z.ZodType<TReturnOut, TReturnIn>;
    tracked?: Tracked;
}): z.ZodType<AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn, TReturnIn, unknown>, AsyncIterable<Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut, TReturnOut, unknown>>
zAsyncIterable
} from './zAsyncIterable';
export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    mySubscription: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<TrackedData<{
            count: number;
        }>, void, unknown>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
router<{
    mySubscription: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<TrackedData<{
            count: number;
        }>, void, unknown>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    mySubscription: SubscriptionProcedure<{
        input: {
            lastEventId?: unknown;
        };
        output: AsyncIterable<TrackedData<{
            count: number;
        }>, void, unknown>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>
router
({
mySubscription: SubscriptionProcedure<{
    input: {
        lastEventId?: unknown;
    };
    output: AsyncIterable<TrackedData<{
        count: number;
    }>, void, unknown>;
    meta: object;
    errorShape: DefaultErrorShape;
}>
mySubscription
: 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.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}, z.core.$strip>>(schema: z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    lastEventId?: unknown;
}, {
    lastEventId?: number | undefined;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(
import zz.
function object<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}>(shape?: {
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
}, z.core.$strip>
object
({
lastEventId: z.ZodOptional<z.ZodCoercedNumber<unknown>>lastEventId: import zz.import coercecoerce.function number<unknown>(params?: string | z.core.$ZodNumberParams): z.ZodCoercedNumber<unknown>number()._ZodNumber<$ZodNumberInternals<unknown>>.min(value: number, params?: string | z.core.$ZodCheckGreaterThanParams): z.ZodCoercedNumber<unknown>min(0).ZodType<any, any, $ZodNumberInternals<unknown>>.optional(): z.ZodOptional<z.ZodCoercedNumber<unknown>>optional(), }), ) .
ProcedureBuilder<object, object, object, { lastEventId?: unknown; }, { lastEventId?: number | undefined; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.output<z.ZodType<AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, z.core.$ZodTypeInternals<AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>>>>(schema: z.ZodType<AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, z.core.$ZodTypeInternals<...>>): ProcedureBuilder<...>
output
(
zAsyncIterable<{
    count: number;
}, {
    count: number;
}, void, void, true>(opts: {
    yield: z.ZodType<{
        count: number;
    }, {
        count: number;
    }, z.core.$ZodTypeInternals<{
        count: number;
    }, {
        count: number;
    }>>;
    return?: z.ZodType<void, void, z.core.$ZodTypeInternals<void, void>> | undefined;
    tracked?: true | undefined;
}): z.ZodType<AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, z.core.$ZodTypeInternals<AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, AsyncIterable<...>>>
zAsyncIterable
({
yield: z.ZodType<{
    count: number;
}, {
    count: number;
}, z.core.$ZodTypeInternals<{
    count: number;
}, {
    count: number;
}>>
yield
: import zz.
function object<{
    count: z.ZodNumber;
}>(shape?: {
    count: z.ZodNumber;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    count: z.ZodNumber;
}, z.core.$strip>
object
({
count: z.ZodNumbercount: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber(), }), tracked?: true | undefinedtracked: true, }), ) .
ProcedureBuilder<object, object, object, { lastEventId?: unknown; }, { lastEventId?: number | undefined; }, AsyncIterable<TrackedEnvelope<{ count: number; }>, void, unknown>, AsyncIterable<...>, false, DefaultErrorShape>.subscription<AsyncIterable<any, void>>(resolver: ProcedureResolver<object, object, object, {
    lastEventId?: number | undefined;
}, AsyncIterable<TrackedEnvelope<{
    count: number;
}>, void, unknown>, AsyncIterable<any, void>>): SubscriptionProcedure<{
    input: {
        lastEventId?: unknown;
    };
    output: AsyncIterable<TrackedData<{
        count: number;
    }>, void, unknown>;
    meta: object;
    errorShape: DefaultErrorShape;
}> (+1 overload)
subscription
(async function* (
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: number | undefined;
}>
opts
) {
let let index: numberindex =
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: number | undefined;
}>
opts
.
ProcedureResolverOptions<object, object, object, { lastEventId?: number | undefined; }>.input: {
    lastEventId?: number | undefined;
}
input
.lastEventId?: number | undefinedlastEventId ?? 0;
while (true) { let index: numberindex++; yield
tracked<{
    count: number;
}>(id: string, data: {
    count: number;
}): TrackedEnvelope<{
    count: number;
}>
tracked
(
var String: StringConstructor
(value?: any) => string
String
(let index: numberindex), {
count: numbercount: let index: numberindex, }); await new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
Promise
((resolve: (value: unknown) => voidresolve) => function setTimeout<[value: unknown]>(callback: (value: unknown) => void, delay?: number, value?: unknown): NodeJS.TimeoutsetTimeout(resolve: (value: unknown) => voidresolve, 1000));
} }), });