@firstflow/sdk on your backend and @firstflow/react in your frontend and one shared id that ties a conversation together.
This page answers the questions that recur during that integration. The mechanism behind most of them is the same, so read the next section first; the rest follow from it.
How the runtime decides what to show
FirstFlow runs eligibility on the server, not in the browser. When a message flows through your wrapped LLM client, the server SDK reports it to the backend, which evaluates triggers, audience, schedule, frequency, and any classifier then composes a widget with Anthropic (claude-sonnet-4-6) and pushes the finished WidgetTree to the browser over Socket.IO. The browser SDK is a passive consumer: it renders what arrives and reports activity back. That single fact explains why you need a backend integration, why the client cannot force a widget to appear, and why the conversation id must match on both sides.
Eligibility lives in ConversationRouterService; flow navigation (flow.next / flow.back) is also resolved server-side the browser only emits intent. See How it works for the full cycle and Triggers & audience for the eligibility rules.
Keys: publishable vs. API key
FirstFlow uses two distinct credentials, and mixing them up is the most common setup mistake. The publishable key (pk_live_…) is browser-safe and belongs to @firstflow/react. You pass it to FirstflowProvider as publishableKey; it authenticates the realtime socket handshake and is fine to ship in client bundles. It is not a secret.
The API key is secret and server-only. The server SDK reads it from the FIRSTFLOW_API_KEY environment variable never hard-code it and never expose it to the browser. It authorizes the calls that report conversation turns, outcomes, and analytics to the backend. Details live in server configuration.
The conversation id (conversationId / sessionId)
One id ties everything together. In the browser you pass it to FirstflowProvider as conversationId; on the server you pass the same value to your wrapped LLM client as sessionId. That shared id is what links the rendered widget, the realtime socket, the stored transcript, the classifier window, and any flow run into a single conversation. You own and generate it.
conversationId is optional and lazy. Omit it until the conversation actually begins the first message then mint a stable id (crypto.randomUUID()), persist it (e.g. localStorage), and reuse it across reloads. With no id yet, the socket connects on user identity alone, so chat_opens experiences still run in user scope. Use a fresh id to start a new conversation. See Conversations & sessions.
Do I need backend changes?
Yes minimal ones. The browser SDK renders experiences, but the backend can only evaluate triggers from messages it can see. To make a conversation observable you wrap your LLM client with@firstflow/sdk and tag each call with three required fields: firstflowAgentId, sessionId, and userId. The wrapped client strips those fields before the request reaches OpenAI or Anthropic, reports the turn to FirstFlow fire-and-forget, then returns the model’s response unchanged. In practice this is a one-line import swap (import { OpenAI } from "@firstflow/sdk/openai" instead of "openai").
If you cannot use the pre-wrapped clients (Vercel AI SDK, a custom gateway), call observe() directly with the same three ids. LangChain and LangGraph apps attach FirstflowCallbackHandler / withFirstflow instead.
Which LLM providers are supported?
The server SDK ships pre-wrapped clients for OpenAI and Anthropic, plus an OpenAI-compatible client for any endpoint that speaks that protocol Ollama, vLLM, and LM Studio and a LangChain integration. Each lives behind its own subpath so you only load the LLM SDK you installed.
The instrumented methods are
chat.completions.create, messages.create, responses.create, and embeddings.create everything else on the client passes through untouched. This is separate from the AI that powers features inside FirstFlow (widget composition, classifiers), which runs on a provider you configure under AI providers. On Next.js, @firstflow/nextjs re-exports the React client and exposes the same server wrappers under /server, /server/openai, /server/anthropic, and /server/aiclient see Next.js.
Why can’t I trigger a widget from the browser?
Because eligibility is evaluated server-side, the browser cannot force a widget to show. The client SDK callsnotifyActivity() to report that the user is active and emits widget.action events back, but the backend decides whether anything qualifies and pushes the result. The one client-initiated path is a command: triggerCommand(commandId, conversationId?) asks the server to run a specific experience or action, and the server still composes and pushes the widget. This is by design it keeps targeting logic in one place and prevents a tampered client from showing experiences to users who do not qualify.
More questions
What does useFirstflow() give me?
What does useFirstflow() give me?
The platform instance:
agentId, workspaceId, an analytics module
(track, identify, page, and optional flush / flushSync),
getUser() / setUser(), notifyActivity(), the commands array
(AgentCommand[]), triggerCommand(commandId, conversationId?),
getConversationId(), and an optional shutdown(). See
useFirstflow.Is there a React Native SDK?
Is there a React Native SDK?
Not currently. The shipping packages are
@firstflow/react,
@firstflow/sdk, @firstflow/nextjs, and @firstflow/widget-kit.Do I import a CSS file for the widget?
Do I import a CSS file for the widget?
No. Styles auto-inject via the package’s
sideEffects. Do not add a CSS
import. Theming arrives from the server’s agent.config event and is
applied automatically; override tokens via theming.Can I self-host? Is anything gated?
Can I self-host? Is anything gated?
Yes, and nothing is gated. The open-source build is the same product as
Cloud no edition split, no billing, no usage limits. Point the browser
apiUrl and the server FIRSTFLOW_API_BASE_URL at your deployment. See
Self-hosting.Is ClickHouse required?
Is ClickHouse required?
No. It is an optional analytics warehouse; the platform degrades gracefully
without it. Conversation analytics and session KPIs are covered at
Analytics.
How do MCP clients authenticate?
How do MCP clients authenticate?
Via OAuth 2.0 + PKCE static API keys are rejected on
/mcp. See
AI Connect overview and API keys.Where does the WidgetTree / Block schema come from?
Where does the WidgetTree / Block schema come from?
From
@firstflow/widget-kit. The backend composes a WidgetTree and the
browser renders it; the shared block primitives are defined there. See
Widget Kit.Next
MountFirstflowProvider and wrap your LLM client with the server SDK. If something is not appearing, Troubleshooting walks through the common causes.