TanStack
Chat UI Recipes

Send the current user to the server

Pass forwardedProps to useAppChat(). Every request from that instance carries it.

tsx
const chat = useAppChat({
  forwardedProps: { tenantId: 'tenant_456' },
})

The value never enters the prompt. The model does not see it.

Client

forwardedProps belongs on the screen, not on chatOptions, because it changes per user.

tsx
import { fetchServerSentEvents } from '@tanstack/ai-react'
import { createChatHook } from '@tanstack/ai-react/ui'

const chatOptions = {
  connection: fetchServerSentEvents('/api/chat'),
}

const { useAppChat, useChatContext } = createChatHook({
  options: chatOptions,
  components: {
    input: () => {
      const chat = useChatContext()
      return (
        <button onClick={() => void chat.sendMessage('What are my invoices?')}>
          Ask
        </button>
      )
    },
    layout: ({ Messages, Input }) => (
      <main>
        <Messages />
        <Input />
      </main>
    ),
    message: ({ Parts }) => <article><Parts /></article>,
  },
  partsComponents: {
    text: ({ part }) => <p>{part.content}</p>,
    fallback: () => null,
  },
})

export function TenantChat({ tenantId }: { tenantId: string }) {
  const chat = useAppChat({
    threadId: `tenant-${tenantId}`,
    forwardedProps: { tenantId },
  })
  return <chat.AppChat />
}

Server

Read it with chatParamsFromRequest, then map it into context. Tools and middleware read context.

tsx
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export async function POST(request: Request) {
  const params = await chatParamsFromRequest(request)

  const tenantId =
    typeof params.forwardedProps.tenantId === 'string'
      ? params.forwardedProps.tenantId
      : undefined
  if (!tenantId) return new Response('Missing tenant', { status: 400 })

  const stream = chat({
    adapter: openaiText('gpt-5.6'),
    messages: params.messages,
    context: { tenantId },
  })

  return toServerSentEventsResponse(stream)
}

Validate it. Always.

forwardedProps comes from the browser. A user can change it.

Never trust it for identity or authorization. Get the user from your session or token on the server, and use forwardedProps only for values that are safe to be wrong:

  1. Derive the user id from the request's own auth, not from the client.
  2. Check every forwarded field against a type or a schema.
  3. Reject the request when a required field is missing.

The example above checks tenantId is a string, then refuses the request without it. In a real app you would also confirm the authenticated user may use that tenant.

One chat per user

Pass threadId alongside forwardedProps when a screen can show more than one chat. Two instances with different thread ids keep separate histories.

Next