> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# FAQ

> Direct answers to the questions that come up while integrating FirstFlow keys, the conversation id, who decides what to show, supported providers, and self-hosting.

FirstFlow is an onboarding and activation layer for AI agent products: it renders tours, surveys, announcements, and AI guides inside your chat UI without you building that UI. Integration is two packages [`@firstflow/sdk`](/server/overview) on your backend and [`@firstflow/react`](/react/overview) 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.

```mermaid theme={null}
flowchart TD
  A["Host app chat<br/>user / assistant message"] -->|"wrapped LLM client"| B["@firstflow/sdk<br/>strips firstflowAgentId / sessionId / userId,<br/>reports the turn"]
  B -->|"POST /v1/conversations/:id/messages (202)"| C["Backend<br/>ConversationRouterService"]
  C --> D{"Eligible?<br/>triggers · audience ·<br/>schedule · frequency · classifier"}
  D -->|no| E["nothing pushed"]
  D -->|yes| F["WidgetCompositionService<br/>Anthropic claude-sonnet-4-6 → WidgetTree"]
  F -->|"widget.show over Socket.IO"| G["@firstflow/react<br/>renders the WidgetTree"]
  G -->|"flow.next / flow.back"| C
  style C fill:#1f2937,color:#fff,stroke:#0ea5e9
  style F fill:#312e81,color:#fff,stroke:#a78bfa
  style G fill:#064e3b,color:#fff,stroke:#34d399
  style D fill:#7c2d12,color:#fff,stroke:#fb923c
```

Eligibility lives in `ConversationRouterService`; flow navigation (`flow.next` / `flow.back`) is also resolved server-side the browser only emits intent. See [How it works](/how-it-works) for the full cycle and [Triggers & audience](/concepts/triggers-and-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`](/react/provider). 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](/server/configuration).

<Warning>
  Never put the API key in frontend code or a `NEXT_PUBLIC_` variable. The browser only needs the publishable key. The API key in a client bundle is a credential leak.
</Warning>

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

```tsx theme={null}
// Browser @firstflow/react
<FirstflowProvider
  agentId="agt_123"
  publishableKey="pk_live_abc"
  user={{ id: user.id, email: user.email }}
  conversationId={conversationId}
>
  {children}
</FirstflowProvider>
```

```ts theme={null}
// Server @firstflow/sdk
import { OpenAI } from "@firstflow/sdk/openai";

const client = new OpenAI(); // reads OPENAI_API_KEY; FIRSTFLOW_API_KEY from env
await client.chat.completions.create({
  firstflowAgentId: "agt_123",
  sessionId: conversationId, // SAME value as the browser conversationId
  userId: user.id,
  model: "gpt-4o",
  messages: [{ role: "user", content: "How do I export my data?" }],
});
```

`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](/concepts/conversations-and-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`](/server/overview) 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"`).

<Warning>
  All three tags are required. If you provide some but not all of `firstflowAgentId` / `sessionId` / `userId`, the call is passed through **unobserved** and the SDK logs a one-time warning. A request with no tags at all is passed through silently that is the intended escape hatch for calls you do not want observed.
</Warning>

If you cannot use the pre-wrapped clients (Vercel AI SDK, a custom gateway), call [`observe()`](/server/observe) directly with the same three ids. LangChain and LangGraph apps attach [`FirstflowCallbackHandler` / `withFirstflow`](/server/langchain) 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.

| Import                     | Use for                                                             |
| -------------------------- | ------------------------------------------------------------------- |
| `@firstflow/sdk/openai`    | OpenAI                                                              |
| `@firstflow/sdk/anthropic` | Anthropic                                                           |
| `@firstflow/sdk/aiclient`  | OpenAI-compatible endpoints (Ollama, vLLM, LM Studio)               |
| `@firstflow/sdk/langchain` | LangChain / LangGraph (`FirstflowCallbackHandler`, `withFirstflow`) |

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](/integrations/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](/react/nextjs).

## 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 calls `notifyActivity()` 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](/react/commands): `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

<AccordionGroup>
  <Accordion title="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](/react/use-firstflow).
  </Accordion>

  <Accordion title="Is there a React Native SDK?">
    Not currently. The shipping packages are `@firstflow/react`,
    `@firstflow/sdk`, `@firstflow/nextjs`, and `@firstflow/widget-kit`.
  </Accordion>

  <Accordion title="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](/react/theming).
  </Accordion>

  <Accordion title="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](/self-hosting/overview).
  </Accordion>

  <Accordion title="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](/features/analytics).
  </Accordion>

  <Accordion title="How do MCP clients authenticate?">
    Via OAuth 2.0 + PKCE static API keys are rejected on `/mcp`. See
    [AI Connect overview](/mcp/overview) and [API keys](/mcp/api-keys).
  </Accordion>

  <Accordion title="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](/widget-kit/overview).
  </Accordion>
</AccordionGroup>

## Next

Mount [`FirstflowProvider`](/react/provider) and wrap your LLM client with the [server SDK](/server/overview). If something is not appearing, [Troubleshooting](/troubleshooting) walks through the common causes.
