---
title: "Per-turn context"
description: "Send live app state with every message via the context prop, and inject it server-side with getContext or trustClientContext — untrusted-by-default so the browser can never spoof authoritative background."
---

Per-turn context makes an agent aware of what the user is *actually doing*
— the route they're on, the record they're viewing, their plan or role — instead
of answering generic Q&A. The widget sends a structured `context` object
alongside every message; the server decides whether and how to fold it into the
model's system prompt.

> **Untrusted by default**
>
> The browser controls the `context` value, so the server treats it as
> **untrusted**. It is injected *only* when the handler opts in — either via a
> server-side `getContext` (authoritative; can validate/merge/override) or the
> `trustClientContext: true` escape hatch. With neither set, the client `context`
> is ignored entirely. Never put secrets in it.

## Sending context from the client

`context` is a plain JSON-serialisable object (`ChatContext = Record<string, unknown>`).
Set it on `<ChatWidget>`; it is sent with every turn, so long-lived sessions never
go stale.

```tsx title="components/assistant.tsx"
'use client';

import { ChatWidget, type ChatContext } from '@mordn/chat-widget';
import { usePathname } from 'next/navigation';

export function Assistant({ plan }: { plan: string }) {
  const route = usePathname();
  return (
    <ChatWidget
      apiBase="/api/chat"
      context={{ route, plan } satisfies ChatContext}
    />
  );
}
```

On its own, this does nothing server-side — the value is inert until the handler
opts in. That is the safe default.

## Option A — `getContext` (authoritative)

The recommended path. Provide `getContext` on `createChatHandler`; it runs after
authentication with the request context and the client's (untrusted) `context`,
and returns the object to inject. Fetch *live server-side data* here — the user's
real plan, open tickets, the record they're viewing — so the injected background
is trustworthy, not client-asserted.

```ts title="app/api/chat/[[...chat]]/route.ts"
export const { GET, POST, DELETE } = createChatHandler({
  getUserId: getChatUserId,
  model: anthropic('claude-sonnet-4-5'),
  getContext: async (ctx, clientContext) => {
    // Trust the VERIFIED user; treat clientContext as a hint you may validate.
    const account = await db.getAccount(ctx.userId);
    const hint = clientContext as { route?: string } | null;

    return {
      plan: account.plan,                    // authoritative, from your DB
      openTickets: account.openTicketCount,  // live server-side data
      route: typeof hint?.route === 'string' ? hint.route : undefined, // validated
    };
  },
});
```

- **`getContext`** · `(ctx: ChatRequestContext, clientContext: unknown) => Record<string, unknown> \| null \| undefined \| Promise<…>`

  Called after authentication. Return a plain JSON-serialisable object to inject as
  authoritative background, or `null`/`undefined` to inject nothing. The merged
  object is folded into the system prompt as a structured JSON block (alongside
  retrieval/memory) and is never echoed back to the client. `clientContext` is
  passed only so you can validate/merge it — never trust it blindly. Per-request,
  not per-session, so answers track the user's real state.

## Option B — `trustClientContext` (escape hatch)

When you have no server-side data to add and the context is genuinely
non-sensitive, `trustClientContext: true` injects the client `context` directly.
It is **off by default** because the browser controls that value
(prompt-injection / data-spoofing risk).

```ts title="app/api/chat/[[...chat]]/route.ts"
export const { GET, POST, DELETE } = createChatHandler({
  getUserId: getChatUserId,
  model: anthropic('claude-sonnet-4-5'),
  trustClientContext: true, // inject the widget's context prop as-is
});
```

- **`trustClientContext`** · `boolean` · default `false`

  Inject the widget's client-supplied `context` prop directly when no `getContext`
  is provided. Enable only for non-sensitive context you're comfortable treating as
  model input. When `getContext` **is** provided it is always authoritative and this
  flag is ignored.

## Precedence

| `getContext` | `trustClientContext` | What gets injected |
|---|---|---|
| provided | — | The object `getContext` returns (authoritative). The flag is ignored. |
| not provided | `true` | The client `context` prop, verbatim. |
| not provided | `false` / unset | Nothing. The client `context` is ignored (the safe default). |

## How it reaches the model

The resolved object is folded into the system prompt as a structured JSON block,
sitting alongside any retrieval and memory blocks — and is never echoed back to
the client. Because it is resolved per request (not per session), a long-lived
widget session always reflects the user's current state rather than whatever was
true when the panel first opened.

> **Warning**
>
> Never put secrets, tokens, or another user's data in the injected context — it
> becomes model input and can surface in a reply. Derive anything sensitive from
> the **verified** `ctx` inside `getContext`, and validate any field you carry over
> from `clientContext`.

See [`createChatHandler`](/docs/api/create-chat-handler) for the full option list
and [Knowledge / RAG](/docs/guides/knowledge) and [Memory](/docs/guides/memory)
for the other blocks that share the system-prompt injection point.
