@firstflow/react) renders whatever the backend pushes; the server SDK (@firstflow/sdk) observes your model calls so the backend can decide what to push. You need three things: an agent’s keys, the two packages installed, and one stable id passed to both sides.
Want the architecture first? How it works explains the
server-side eligibility model in two minutes. This page is the hands-on path.
How the pieces connect
FirstFlow is server-authoritative. Eligibility triggers, audience, schedule, frequency, and any classifier is evaluated on the backend insideConversationRouterService. When a user qualifies, the backend composes a WidgetTree (Anthropic claude-sonnet-4-6) and pushes the finished widget down a Socket.IO connection. The browser SDK is a passive consumer: it opens the socket, renders what arrives, and reports activity back. You cannot trigger a widget purely from the client your server’s observed LLM calls are what feed the trigger engine.
That is why both SDKs matter, and why they must agree on one identifier. The browser passes it as conversationId; the server passes the same string as sessionId. That shared id is what links the widget, the realtime socket, the stored transcript, the branch-decision classifier, and any flow run into a single conversation. Get this one value consistent and everything downstream just works.
The dotted edge is the only thing the browser decides on its own: it emits the user’s intent (flow.next, flow.back). The backend’s FlowNavigationListener advances the flow and composes the next tree. Navigation is server-side, just like eligibility.
Get your keys
Each agent is one workspace for one AI product. Create one in the dashboard, then open Settings → SDK integration. You will use three values: the agent ID and publishable key (pk_live_…) in the browser, and the API key (FIRSTFLOW_API_KEY) on the server. The first two are browser-safe; the publishable key is designed to ship in client code. The API key is a secret keep it server-side only.
Render experiences (browser SDK)
1
Install @firstflow/react
@firstflow/nextjs instead; it re-exports the React surface
plus server entry points.2
Mount the provider and widget
Wrap your app once with The
FirstflowProvider and mount
FirstflowWidget next to your chat. The provider opens the
realtime socket and identifies the user; the widget renders whatever the
backend pushes.- React (Vite)
- Next.js (App Router)
user prop carries your already-authenticated user { id, email?, name?, traits? }. FirstFlow trusts this value; your auth provider is responsible for verifying who the user is. Traits flow into audience targeting and analytics. The provider throws if agentId or publishableKey is empty.3
Mint the conversation id lazily
conversationId is optional and lazy you own it. Leave it empty until
the conversation actually begins (the first message), then mint a stable id,
persist it, and reuse it across reloads.chat_opens experiences (tours, surveys, announcements) still run in user
scope. The moment a real id appears, the socket reconnects bound to it. Use a
fresh id to start a new conversation. Full rules are in
Conversations & sessions.Observe LLM calls (server SDK)
The backend can only decide what to show if it sees the conversation. The server SDK gives you a drop-in client that mirrors the OpenAI / Anthropic SDK exactly, plus three required tags on each call.1
Install @firstflow/sdk and set the secret
.env (server-only)
FIRSTFLOW_API_KEY from the environment never hard-code it,
never expose it to the browser. Self-hosting? Also set
FIRSTFLOW_API_BASE_URL to your backend.2
Swap your client and tag every call
Import the pre-wrapped client from the peer-isolated subpath for your
provider, then add The wrapper is a transparent proxy. It instruments
firstflowAgentId, sessionId, and userId to each
request body.chat.completions.create,
messages.create, responses.create, and embeddings.create, strips the
three FirstFlow fields from the body before the request reaches OpenAI or
Anthropic, and fires fire-and-forget observe hooks with model, tokens,
latency, and finish reason once the call completes. Streaming is handled
automatically for OpenAI it injects stream_options: { include_usage: true }
so the final chunk carries token counts.sessionId that does not match the browser’s conversationId; when that happens the widget and the transcript end up in two different conversations.
If you cannot use the pre-wrapped clients (Vercel AI SDK, a custom gateway), call observe() directly with the same { firstflowAgentId, sessionId, userId } shape. The full server surface wrapClient, outcome, track, trace is in the Server SDK overview.
Create and publish an experience
With both SDKs wired, build something for the backend to push.1
Create the experience
In your agent, go to Experiences → New experience and pick a type:
guide, tour, survey, or announcement. The type is immutable after
creation. A guide is a single AI-composed widget driven by a flow graph; the
others are multi-step authored experiences. See
Experiences.2
Build the flow
Use the visual builder, or describe the flow and let AI generate it. The
graph is
{ nodes, edges }; the backend walks it to a Result node and
composes the widget. See Flows & nodes.3
Set triggers and audience
Choose when it fires (e.g.
after_user_message, chat_opens, or a
classifier) and who sees it (all users or a segment).
This is the server-side eligibility you saw above. See
Triggers & audience.4
Publish
Set status to Active. The next matching conversation receives it.
Verify it works
Open your app and start a conversation so the server SDK observes a turn. Two signals tell you the loop is closed:- The widget appears. A matching trigger fires, the backend composes the
WidgetTree, andFirstflowWidgetrenders it. If nothing shows, the eligibility rules did not match that decision is server-side, so check the experience’s triggers and audience, not the browser. - The conversation is observed. In the dashboard, the experience’s analytics show the session and turns. If they are empty, the LLM call was likely tagged incompletely re-check that all three server tags are present and that
sessionIdequals the browser’sconversationId.
[Firstflow] realtime connect failed:, which usually means a wrong publishableKey, agentId, or (when self-hosting) apiUrl.
Next
Go deeper on the side you’ll touch next: the Browser SDK for the widget, identity, commands, and theming, or the Server SDK for outcomes, traces, and configuration. The shared model behind both lives in Core concepts, and you can run the whole platform yourself via Self-hosting.Browser SDK
Provider, widget, identity, commands, theming.
Server SDK
Wrapping clients, traces, outcomes, analytics.