Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Persistence (Advanced)
Embeddings
Reranking
Evaluate
Sandboxes (Advanced)
Class References
Function References
Interface References
Type Alias References
Variable References
Getting Started

Quick Start

You want a streaming chat in your app. TanStack AI streams from a server route. The hook for your framework renders the tokens.

For a React + Start app with BYOK, open Basic Chat.

For an MCP server with a tool, a resource, and a prompt, open Build an MCP Server.

For an image from a prompt, open Generate Image.

For a table that fills as JSON streams, open Streaming Structured Table.

For a blog desk where a researcher and an SEO agent run together and a writer uses their notes, open Subagents.

Tip

If you do not want a key per provider, OpenRouter gives you 300+ models with one API key.

React Native or Expo needs an absolute server URL and an XHR transport. See Quick Start: React Native.

No UI: see Quick Start: Server Only.

1. Install

sh
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openai

2. Stream from the server

Call chat(). Then wrap the result with toServerSentEventsResponse.

typescript
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: openaiText("gpt-5.6"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: openaiText("gpt-5.6"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}

This works with TanStack Start, Next.js, SvelteKit, Hono, and any host that returns a Web Response.

A Remix controller action can return this same Response.

If your server is Node streams (Express), see Quick Start: Server Only.

Put the API key on the server:

sh
OPENAI_API_KEY=your-openai-api-key
OPENAI_API_KEY=your-openai-api-key

The adapter reads OPENAI_API_KEY at runtime. Do not send this key to the browser.

If you do not want a server key, see Bring Your Own Key.

3. Render the chat

Call useChat from @tanstack/ai-react. Hold the composer text in useState. Pass it to sendMessage.

tsx
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage, isLoading, stop } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
  });

  return (
    <>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={index}>{part.content}</p> : null,
          )}
        </div>
      ))}
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (input.trim() === "") {
            return;
          }
          sendMessage(input);
          setInput("");
        }}
      >
        <input
          value={input}
          onChange={(event) => setInput(event.target.value)}
        />
        {isLoading ? (
          <button type="button" onClick={stop}>
            Stop
          </button>
        ) : (
          <button type="submit">Send</button>
        )}
      </form>
    </>
  );
}
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage, isLoading, stop } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
  });

  return (
    <>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={index}>{part.content}</p> : null,
          )}
        </div>
      ))}
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (input.trim() === "") {
            return;
          }
          sendMessage(input);
          setInput("");
        }}
      >
        <input
          value={input}
          onChange={(event) => setInput(event.target.value)}
        />
        {isLoading ? (
          <button type="button" onClick={stop}>
            Stop
          </button>
        ) : (
          <button type="submit">Send</button>
        )}
      </form>
    </>
  );
}

messages updates as chunks arrive. isLoading is true while the run is in flight.

See the React API.

Send a message. Tokens show up in the UI.

Later