Skip to content
mordn
Esc
↑↓navigate↵open⌘Jpreview
On this page

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
});
trustClientContextboolean
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.

See createChatHandler for the full option list and Knowledge / RAG and Memory for the other blocks that share the system-prompt injection point.