Per-turn context
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.
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
});
trustClientContextbooleanfalseInject 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.
See createChatHandler for the full option list
and Knowledge / RAG and Memory
for the other blocks that share the system-prompt injection point.