Per-turn context
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.
'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.
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).
export const { GET, POST, DELETE } = createChatHandler({
getUserId: getChatUserId,
model: anthropic('claude-sonnet-4-5'),
trustClientContext: true, // inject the widget's context prop as-is
});trustClientContextbooleandefault: falseInject 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 for the full option list
and Knowledge / RAG and Memory
for the other blocks that share the system-prompt injection point.