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.

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

code
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).

code
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
});
trustClientContextbooleandefault: 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

getContexttrustClientContextWhat gets injected
providedThe object getContext returns (authoritative). The flag is ignored.
not providedtrueThe client context prop, verbatim.
not providedfalse / unsetNothing. 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.