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

# Quickstart

> Install both SDKs, link the browser and server with one shared conversation id, and ship your first server-pushed experience.

By the end of this guide your AI product renders FirstFlow experiences in the browser and reports every LLM turn from your backend joined by a single conversation id you own. The browser SDK ([`@firstflow/react`](/react/overview)) renders whatever the backend pushes; the server SDK ([`@firstflow/sdk`](/server/overview)) 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.

<Note>
  Want the architecture first? [How it works](/how-it-works) explains the
  server-side eligibility model in two minutes. This page is the hands-on path.
</Note>

## How the pieces connect

FirstFlow is **server-authoritative**. Eligibility triggers, audience, schedule, frequency, and any classifier is evaluated on the backend inside `ConversationRouterService`. 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.

```mermaid theme={null}
flowchart TD
  U([User sends a message]):::user --> S
  S["@firstflow/sdk wraps your<br/>OpenAI/Anthropic client"]:::sdk -->|"observe(): firstflowAgentId<br/>+ sessionId + userId"| B
  B["backend ConversationRouterService<br/>evaluates triggers + audience + classifier"]:::backend -->|eligible| C
  C["WidgetCompositionService<br/>Anthropic claude-sonnet-4-6 → WidgetTree"]:::backend -->|push over Socket.IO| R
  R["@firstflow/react FirstflowWidget<br/>renders the WidgetTree"]:::browser --> U
  R -.->|"widget.action (flow.next / flow.back)"| B

  classDef user fill:#fde68a,stroke:#92400e,color:#1c1917;
  classDef sdk fill:#bbf7d0,stroke:#166534,color:#1c1917;
  classDef backend fill:#e9d5ff,stroke:#6b21a8,color:#1c1917;
  classDef browser fill:#fecdd3,stroke:#9f1239,color:#1c1917;
```

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](/concepts/agents) is one workspace for one AI product. Create one in the [dashboard](https://app.firstflow.app), then open **Settings → SDK integration**.

<Snippet file="where-to-find-keys.mdx" />

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)

<Steps>
  <Step title="Install @firstflow/react">
    <CodeGroup>
      ```bash npm theme={null}
      npm install @firstflow/react
      ```

      ```bash pnpm theme={null}
      pnpm add @firstflow/react
      ```

      ```bash yarn theme={null}
      yarn add @firstflow/react
      ```
    </CodeGroup>

    Styles auto-inject there is no CSS file to import. On Next.js, install
    [`@firstflow/nextjs`](/react/nextjs) instead; it re-exports the React surface
    plus server entry points.
  </Step>

  <Step title="Mount the provider and widget">
    Wrap your app once with [`FirstflowProvider`](/react/provider) and mount
    [`FirstflowWidget`](/react/widget) next to your chat. The provider opens the
    realtime socket and identifies the user; the widget renders whatever the
    backend pushes.

    <Tabs>
      <Tab title="React (Vite)">
        ```tsx theme={null}
        import { FirstflowProvider, FirstflowWidget } from "@firstflow/react";

        export default function App() {
          return (
            <FirstflowProvider
              agentId={import.meta.env.VITE_FIRSTFLOW_AGENT_ID}
              publishableKey={import.meta.env.VITE_FIRSTFLOW_PUBLISHABLE_KEY}
              user={{ id: currentUser.id, traits: { plan: currentUser.plan } }}
              conversationId={conversationId}
            >
              <ChatApp />
              <FirstflowWidget />
            </FirstflowProvider>
          );
        }
        ```
      </Tab>

      <Tab title="Next.js (App Router)">
        ```tsx theme={null}
        // app/providers.tsx
        "use client";
        import { FirstflowProvider, FirstflowWidget } from "@firstflow/nextjs";

        export function Providers({ children }: { children: React.ReactNode }) {
          return (
            <FirstflowProvider
              agentId={process.env.NEXT_PUBLIC_FIRSTFLOW_AGENT_ID!}
              publishableKey={process.env.NEXT_PUBLIC_FIRSTFLOW_PUBLISHABLE_KEY!}
              user={{ id: currentUser.id }}
              conversationId={conversationId}
            >
              {children}
              <FirstflowWidget />
            </FirstflowProvider>
          );
        }
        ```
      </Tab>
    </Tabs>

    The `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](/concepts/audience) and [analytics](/react/analytics). The provider throws if `agentId` or `publishableKey` is empty.
  </Step>

  <Step title="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.

    ```tsx theme={null}
    // On the first message of a new conversation:
    const conversationId = crypto.randomUUID(); // e.g. "ses_a1b2c3d4"
    localStorage.setItem("ff_conversation_id", conversationId);
    ```

    With no conversation yet, the socket connects on user identity alone, so
    `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](/concepts/conversations-and-sessions).
  </Step>
</Steps>

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

<Steps>
  <Step title="Install @firstflow/sdk and set the secret">
    <CodeGroup>
      ```bash npm theme={null}
      npm install @firstflow/sdk
      ```

      ```bash pnpm theme={null}
      pnpm add @firstflow/sdk
      ```

      ```bash yarn theme={null}
      yarn add @firstflow/sdk
      ```
    </CodeGroup>

    ```bash .env (server-only) theme={null}
    FIRSTFLOW_API_KEY=ff_live_...
    ```

    The SDK reads `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.
  </Step>

  <Step title="Swap your client and tag every call">
    Import the pre-wrapped client from the peer-isolated subpath for your
    provider, then add `firstflowAgentId`, `sessionId`, and `userId` to each
    request body.

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

      const client = new OpenAI(); // reads OPENAI_API_KEY as usual

      await client.chat.completions.create({
        firstflowAgentId: "agt_…",
        sessionId,        // SAME value as the browser's conversationId
        userId: user.id,
        model: "gpt-4o",
        messages,
      });
      ```

      ```ts Anthropic theme={null}
      import { Anthropic } from "@firstflow/sdk/anthropic";

      const client = new Anthropic(); // reads ANTHROPIC_API_KEY as usual

      await client.messages.create({
        firstflowAgentId: "agt_…",
        sessionId,        // SAME value as the browser's conversationId
        userId: user.id,
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        messages,
      });
      ```

      ```ts OpenAI-compatible theme={null}
      // Ollama, vLLM, LM Studio, or any OpenAI-compatible endpoint
      import { AIClient } from "@firstflow/sdk/aiclient";

      const ai = new AIClient({ baseURL: "http://localhost:11434/v1", apiKey: "…" });

      await ai.chat.completions.create({
        firstflowAgentId: "agt_…",
        sessionId,
        userId: user.id,
        model: "llama3.1",
        messages,
      });
      ```

      ```ts LangChain theme={null}
      import { FirstflowCallbackHandler } from "@firstflow/sdk/langchain";

      await model.invoke(messages, {
        callbacks: [new FirstflowCallbackHandler()],
        metadata: { firstflowAgentId: "agt_…", sessionId, userId: user.id },
      });
      ```
    </CodeGroup>

    The wrapper is a transparent proxy. It instruments `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.
  </Step>
</Steps>

All three tags are required together. The wrapper never infers identity: tag a call with some but not all of them and the call is **passed through unobserved** plus a one-time console warning your conversation silently never reaches the trigger engine. The most common mistake is a `sessionId` that does not match the browser's `conversationId`; when that happens the widget and the transcript end up in two different conversations.

<Warning>
  Partial tagging is silent. If a request carries `firstflowAgentId` and
  `userId` but no `sessionId`, FirstFlow strips the fields, runs the model call
  normally, and observes **nothing** you only get a single warning the first
  time. Always pass all three, and confirm `sessionId === conversationId`.
</Warning>

If you cannot use the pre-wrapped clients (Vercel AI SDK, a custom gateway), call [`observe()`](/server/observe) directly with the same `{ firstflowAgentId, sessionId, userId }` shape. The full server surface [`wrapClient`](/server/wrap-llm-client), [`outcome`](/server/outcomes), [`track`](/server/analytics), [`trace`](/server/traces) is in the [Server SDK overview](/server/overview).

## Create and publish an experience

With both SDKs wired, build something for the backend to push.

<Steps>
  <Step title="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](/concepts/experiences).
  </Step>

  <Step title="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](/concepts/flows-and-nodes).
  </Step>

  <Step title="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](/concepts/audience)).
    This is the server-side eligibility you saw above. See
    [Triggers & audience](/concepts/triggers-and-audience).
  </Step>

  <Step title="Publish">
    Set status to **Active**. The next matching conversation receives it.
  </Step>
</Steps>

## 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`, and `FirstflowWidget` renders 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](/features/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 `sessionId` equals the browser's `conversationId`.

A useful sanity check: open your browser console after mounting the provider. A failed realtime connection logs `[Firstflow] realtime connect failed:`, which usually means a wrong `publishableKey`, `agentId`, or (when self-hosting) `apiUrl`.

<Snippet file="self-host-base-url.mdx" />

## Next

Go deeper on the side you'll touch next: the [Browser SDK](/react/overview) for the widget, identity, [commands](/react/commands), and [theming](/react/theming), or the [Server SDK](/server/overview) for [outcomes](/server/outcomes), [traces](/server/traces), and [configuration](/server/configuration). The shared model behind both lives in [Core concepts](/concepts), and you can run the whole platform yourself via [Self-hosting](/self-hosting/overview).

<CardGroup cols={2}>
  <Card title="Browser SDK" icon="react" href="/react/overview">
    Provider, widget, identity, commands, theming.
  </Card>

  <Card title="Server SDK" icon="server" href="/server/overview">
    Wrapping clients, traces, outcomes, analytics.
  </Card>
</CardGroup>
