# 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.

> [!TIP]
> If you do not want a key per provider, [OpenRouter](../adapters/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](./quick-start-react-native).

No UI: see [Quick Start: Server Only](./quick-start-server).

## 1. Install

```sh
npm i @tanstack/ai @tanstack/ai-remix @tanstack/ai-openai remix
```

## 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);
}
```

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](./quick-start-server).

Put the API key on the server:

```bash
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](../advanced/byok).

## 3. Render the chat

Call `createChat(handle, options)` from `@tanstack/ai-remix` inside a `clientEntry` island. Put `connection` and `tools` in setup. They are not serializable `clientEntry` props.

`@tanstack/ai-remix` publishes uncompiled source. Remix compiles JSX through `jsxImportSource` `remix/ui`. Bind the form with the Remix `on` mixin.

```tsx ignore
import { createChat, fetchServerSentEvents } from "@tanstack/ai-remix";
import { clientEntry, on, type Handle } from "remix/ui";

export const Chat = clientEntry(
  import.meta.url,
  function Chat(handle: Handle) {
    const chat = createChat(handle, {
      connection: fetchServerSentEvents("/api/chat"),
    });

    return () => (
      <div>
        {chat.messages.map((message) => (
          <div key={message.id}>
            {message.parts.map((part, index) =>
              part.type === "text" ? <p key={index}>{part.content}</p> : null,
            )}
          </div>
        ))}
        <form
          mix={on("submit", (event) => {
            event.preventDefault();
            const form = event.currentTarget;
            const text = String(
              new FormData(form).get("message") ?? "",
            ).trim();
            if (text === "") {
              return;
            }
            form.reset();
            void chat.sendMessage(text);
          })}
        >
          <input name="message" disabled={chat.isLoading} />
          {chat.isLoading ? (
            <button type="button" mix={on("click", () => chat.stop())}>
              Stop
            </button>
          ) : (
            <button type="submit">Send</button>
          )}
        </form>
      </div>
    );
  },
);
```

Read `chat.messages` and `chat.isLoading` in the render function so each paint sees the latest values.

See the [Remix API](../api/ai-remix). For a typed headless chat UI, see [Remix Chat UI](../ui/remix).

Send a message. Tokens show up in the UI.

## Later

- [Tools](../tools/tools) for function calling
- [Streaming](../chat/streaming) for cancel, callbacks, and transports
- [Adapters](../adapters/openai) for other providers
