---
title: "Build a Streaming AI Chat Agent with tRPC v11 + Next.js 15 (2026)"
description: "Use tRPC v11 async generators to stream OpenAI tokens over SSE through a fully type-safe stack. Real Next.js App Router code, no WebSockets needed."
canonical: https://callsphere.ai/blog/vw8h-build-ai-chat-agent-trpc-nextjs-streaming-2026
category: "AI Engineering"
tags: ["tRPC", "Next.js", "Chat Agent", "TypeScript", "SSE"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-07T22:23:14.415Z
---

# Build a Streaming AI Chat Agent with tRPC v11 + Next.js 15 (2026)

> Use tRPC v11 async generators to stream OpenAI tokens over SSE through a fully type-safe stack. Real Next.js App Router code, no WebSockets needed.

> **TL;DR** — tRPC v11 added `async function*` procedures that stream over Server-Sent Events. Wrap an OpenAI `stream: true` call and you get end-to-end type-safe token streaming with zero WebSocket plumbing.

## What you'll build

A Next.js 15 App Router chat UI with a tRPC `chat.stream` mutation that yields OpenAI tokens one at a time. The client uses the new `useSubscription` hook so React renders deltas as they arrive — no manual `fetch` + reader code.

## Prerequisites

1. Next.js 15.x + App Router, TypeScript 5.5+.
2. `@trpc/server@^11`, `@trpc/client@^11`, `@trpc/react-query@^11`, `zod@^3.23`.
3. `openai@^4.70` and an API key.

## Architecture

```mermaid
flowchart LR
  UI[React useSubscription] -- SSE --> RT[tRPC HTTP route]
  RT -- async generator --> OA[OpenAI stream: true]
  OA -- delta --> RT --> UI
```

## Step 1 — tRPC router with async generator

```ts
import { z } from "zod";
import OpenAI from "openai";
import { router, publicProcedure } from "./trpc";

const oa = new OpenAI();

export const chatRouter = router({
  stream: publicProcedure
    .input(z.object({ messages: z.array(z.object({
      role: z.enum(["user","assistant","system"]),
      content: z.string(),
    })) }))
    .subscription(async function* ({ input, signal }) {
      const s = await oa.chat.completions.create({
        model: "gpt-4o-mini",
        messages: input.messages,
        stream: true,
      }, { signal });
      for await (const chunk of s) {
        const delta = chunk.choices[0]?.delta?.content;
        if (delta) yield { type: "delta", text: delta };
      }
      yield { type: "done" };
    }),
});
```

## Step 2 — Mount the route handler

```ts
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/router";

const handler = (req: Request) =>
  fetchRequestHandler({ endpoint: "/api/trpc", req, router: appRouter,
                       createContext: () => ({}) });

export { handler as GET, handler as POST };
```

## Step 3 — Client provider with SSE link

```ts
import { createTRPCReact, httpSubscriptionLink, splitLink, httpBatchLink }
  from "@trpc/react-query";

export const trpc = createTRPCReact();
export const trpcClient = trpc.createClient({
  links: [splitLink({
    condition: (op) => op.type === "subscription",
    true:  httpSubscriptionLink({ url: "/api/trpc" }),
    false: httpBatchLink({ url: "/api/trpc" }),
  })],
});
```

## Step 4 — useSubscription in a component

```tsx
"use client";
import { useState } from "react";
import { trpc } from "@/lib/trpc";

export function Chat() {
  const [text, setText] = useState("");
  const [reply, setReply] = useState("");
  const [run, setRun] = useState([]);

trpc.chat.stream.useSubscription(
    { messages: run },
    { enabled: run.length > 0,
      onData: (e) => e.type === "delta" && setReply((r) => r + e.text) },
  );

return (
     {
      e.preventDefault();
      setReply("");
      setRun([{ role: "user", content: text }]);
    }}>
       setText(e.target.value)} />

```
{reply}
```

  );
}
```

## Step 5 — Cancel mid-stream

When the user navigates away, React Query unmounts the subscription. tRPC v11 propagates the abort signal into the OpenAI client (`{ signal }`), which cancels the upstream request — saving tokens.

## Pitfalls

- **Edge runtime**: `runtime = "edge"` works for SSE but limits some Node-only OpenAI helpers — pick `runtime = "nodejs"` if you need `stream.controller`.
- **Buffering proxies**: NGINX, Cloudflare, Vercel default proxies can buffer SSE — set `X-Accel-Buffering: no` or use `responseInit: { headers: { "X-Accel-Buffering": "no" } }`.
- **JSON-line vs SSE**: tRPC v11 auto-picks SSE for browsers; if you call from a non-browser client, switch to `httpLink` and parse JSON-lines.

## How CallSphere does this in production

CallSphere's Sales product (Node.js 20 + React 18 + Vite) ships a near-identical tRPC pipe for real-time SDR chat across **37 agents**, **90+ tools**, and **115+ DB tables** — used in 6 verticals at **$149/$499/$1,499** pricing with a **14-day no-card trial** and **22% affiliate**. p95 first-token latency is ~280ms.

## FAQ

**Does this work with React Server Components?** The streaming part is a client component, but the trigger can be a server action that primes `run`.

**Why not WebSockets?** SSE is HTTP/1.1 friendly, survives serverless, and tRPC v11 picked it as the default for that reason.

**Can I stream tool calls?** Yes — yield `{ type: "tool_call", name, args }` from the generator and render a tool UI on the client.

**What about Vercel AI SDK?** AI SDK and tRPC are complementary — wrap `streamText` inside a tRPC subscription if you want both.

## Sources

- tRPC v11 - Subscriptions over SSE - [https://trpc.io/docs/server/subscriptions](https://trpc.io/docs/server/subscriptions)
- tRPC examples-next-sse-chat - [https://github.com/trpc/examples-next-sse-chat](https://github.com/trpc/examples-next-sse-chat)
- OpenAI streaming API - [https://platform.openai.com/docs/api-reference/streaming](https://platform.openai.com/docs/api-reference/streaming)
- Vercel AI SDK + tRPC discussion - [https://github.com/vercel/ai/discussions/3236](https://github.com/vercel/ai/discussions/3236)

---

Source: https://callsphere.ai/blog/vw8h-build-ai-chat-agent-trpc-nextjs-streaming-2026
