> ## 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 the FirstFlow SDK, point it at your own Supabase project, and render your first experience no server to deploy.

This is the whole setup: install the packages, apply the migrations to a Supabase project you own, author one experience in your repo, mount the provider, and (optionally) wrap your LLM client. The runtime evaluates everything in the browser, so there is no FirstFlow service to run.

## Prerequisites

* **Node 18+** and **pnpm 9** (the repo's package manager).
* **git** to clone the repo.
* A **Supabase project** free tier is fine. Create one at [supabase.com/dashboard](https://supabase.com/dashboard).

You consume FirstFlow by **cloning the repo and building your app inside its workspace**. The packages aren't published to npm; your app references them locally with `workspace:*`.

## Steps

<Steps>
  <Step title="Fork, clone, and build">
    Fork firstflow-oss (so you can commit your own app into it), clone your fork,
    install, and build the packages once.

    ```bash theme={null}
    git clone https://github.com/<you>/firstflow-oss
    cd firstflow-oss
    pnpm install
    pnpm build      # builds @firstflow/runtime, runtime-server, config-schema
    ```
  </Step>

  <Step title="Add your app to the workspace">
    Scaffold your app in a new folder inside the repo with your stack's usual tool:

    <Tabs>
      <Tab title="Next.js">
        ```bash theme={null}
        pnpm create next-app@latest apps/web
        ```
      </Tab>

      <Tab title="Vite + React">
        ```bash theme={null}
        pnpm create vite@latest apps/web -- --template react-ts
        ```
      </Tab>
    </Tabs>

    Add its folder to the workspace and reference the packages with `workspace:*`:

    ```yaml pnpm-workspace.yaml theme={null}
    packages:
      - "packages/*"
      - "apps/*"            # add the folder your app lives in
    ```

    ```json apps/web/package.json theme={null}
    {
      "dependencies": {
        "@firstflow/runtime": "workspace:*",
        "@firstflow/runtime-server": "workspace:*",
        "@firstflow/config-schema": "workspace:*",
        "@supabase/supabase-js": "^2.45.0"
      }
    }
    ```

    Then run `pnpm install` from the repo root. Only `@firstflow/runtime` +
    `@supabase/supabase-js` are required; add `@firstflow/runtime-server` for LLM
    observability.

    <Tip>
      `workspace:*` only resolves **inside** this monorepo that's why your app
      lives here. Re-run `pnpm build` after editing any package so your app picks
      up the change.
    </Tip>
  </Step>

  <Step title="Set up Supabase">
    Create a project, then paste
    [`packages/config-schema/supabase/schema.sql`](https://github.com/firstflowdev/firstflow-oss/blob/main/packages/config-schema/supabase/schema.sql)
    into the Supabase SQL editor and run it **once** one file creates the whole
    schema. Full walkthrough in [Set up Supabase](/self-hosting/supabase-cloud).

    Then put the keys in your environment. Three values: the project URL and
    `anon` key are public (browser-safe); the `service_role` key is server-only.
    **The variable *names* depend on your bundler** public values must carry
    its client-env prefix:

    <Tabs>
      <Tab title="Next.js">
        ```bash .env.local theme={null}
        NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
        NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
        SUPABASE_SERVICE_ROLE_KEY=eyJ...        # server-only no NEXT_PUBLIC_ prefix
        ```
      </Tab>

      <Tab title="Vite + React">
        ```bash .env theme={null}
        VITE_SUPABASE_URL=https://xxxxx.supabase.co
        VITE_SUPABASE_ANON_KEY=eyJ...
        # service_role key lives on your server only never in a VITE_ var
        ```
      </Tab>

      <Tab title="Create React App">
        ```bash .env theme={null}
        REACT_APP_SUPABASE_URL=https://xxxxx.supabase.co
        REACT_APP_SUPABASE_ANON_KEY=eyJ...
        # service_role key lives on your server only never in a REACT_APP_ var
        ```
      </Tab>
    </Tabs>

    The `NEXT_PUBLIC_` / `VITE_` / `REACT_APP_` prefix is just how each bundler
    exposes a value to the browser. The runtime itself takes plain strings see
    [Configuration](/self-hosting/configuration).
  </Step>

  <Step title="Author your first experience">
    Create `firstflow/experiences.json` in your repo. This file is your source of
    truth it is versioned in git and imported at build time.

    ```json firstflow/experiences.json theme={null}
    [
      {
        "id": "exp_welcome",
        "name": "Welcome",
        "type": "guide",
        "status": "active",
        "trigger": { "on": "page_load", "delay": 1 },
        "frequency": "once_per_user",
        "steps": [
          {
            "id": "hi",
            "type": "message",
            "title": "Hello",
            "content": "Welcome to my app!",
            "cta": "Get started"
          },
          { "id": "done", "type": "thank_you", "title": "You're all set", "confetti": true }
        ]
      }
    ]
    ```

    The full step and action reference is in
    [`config-schema/SCHEMA.md`](https://github.com/firstflowdev/firstflow-oss/blob/main/packages/config-schema/SCHEMA.md).
  </Step>

  <Step title="Mount the provider and widget">
    `@firstflow/runtime` is plain React it works in **any** stack. Wrap your app
    once; only how you read the env differs. The default Supabase persistence
    adapter ships with `@firstflow/runtime/supabase`.

    <Tabs>
      <Tab title="Next.js (App Router)">
        ```tsx app/firstflow.tsx theme={null}
        "use client";
        import "@firstflow/runtime/styles.css";
        import { FirstflowProvider, FirstflowWidget } from "@firstflow/runtime";
        import { createSupabasePersistence } from "@firstflow/runtime/supabase";
        import { createClient } from "@supabase/supabase-js";
        import experiences from "@/firstflow/experiences.json";

        const supabase = createClient(
          process.env.NEXT_PUBLIC_SUPABASE_URL!,
          process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        );
        const persistence = createSupabasePersistence(supabase);

        export function Firstflow({ children }: { children: React.ReactNode }) {
          return (
            <FirstflowProvider experiences={experiences} persistence={persistence}>
              {children}
              <FirstflowWidget />
            </FirstflowProvider>
          );
        }
        ```

        Mount `<Firstflow>` in your root layout. The provider is a client
        component (it uses hooks + `localStorage`). See [Next.js](/react/nextjs).
      </Tab>

      <Tab title="Vite + React">
        ```tsx App.tsx theme={null}
        import "@firstflow/runtime/styles.css";
        import { FirstflowProvider, FirstflowWidget } from "@firstflow/runtime";
        import { createSupabasePersistence } from "@firstflow/runtime/supabase";
        import { createClient } from "@supabase/supabase-js";
        import experiences from "./firstflow/experiences.json";

        const supabase = createClient(
          import.meta.env.VITE_SUPABASE_URL,
          import.meta.env.VITE_SUPABASE_ANON_KEY,
        );
        const persistence = createSupabasePersistence(supabase);

        export default function App() {
          return (
            <FirstflowProvider experiences={experiences} persistence={persistence}>
              <YourApp />
              <FirstflowWidget />
            </FirstflowProvider>
          );
        }
        ```
      </Tab>
    </Tabs>

    Pass `user={{ id, traits }}` from your own auth for signed-in users. It's
    **optional** omit it and the runtime uses a stable per-browser
    [anonymous id](/self-hosting/auth-and-orgs) so caps and resume still work.
  </Step>

  <Step title="(Optional) Observe your LLM calls">
    `@firstflow/runtime-server` is framework-agnostic Node set it up once with
    your **server-only** service-role key, then wrap your LLM client wherever you
    call it. The setup is identical across backends:

    ```ts firstflow.server.ts theme={null}
    import { Firstflow } from "@firstflow/runtime-server";
    import { createSupabaseLLMPersistence } from "@firstflow/runtime-server/supabase";
    import { createClient } from "@supabase/supabase-js";
    import Anthropic from "@anthropic-ai/sdk";

    const supabase = createClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY!, // server-only, bypasses RLS
    );
    export const ff = new Firstflow({
      persistence: createSupabaseLLMPersistence(supabase),
      captureContent: true,
    });
    export const ai = ff.wrap(new Anthropic());
    ```

    Then call it from your handler:

    <Tabs>
      <Tab title="Next.js route handler">
        ```ts app/api/chat/route.ts theme={null}
        import { ai } from "@/firstflow.server";

        export async function POST(req: Request) {
          const { userId, sessionId, message } = await req.json();
          const reply = await ai.messages.create({
            model: "claude-haiku-4-5",
            messages: [{ role: "user", content: message }],
            firstflow: { userId, sessionId },   // sessionId = the shared conversation id
          } as any);
          return Response.json({ reply });
        }
        ```
      </Tab>

      <Tab title="Express">
        ```ts theme={null}
        import { ai } from "./firstflow.server";

        app.post("/api/chat", async (req, res) => {
          const { userId, sessionId, message } = req.body;
          const reply = await ai.messages.create({
            model: "claude-haiku-4-5",
            messages: [{ role: "user", content: message }],
            firstflow: { userId, sessionId },
          } as any);
          res.json({ reply });
        });
        ```
      </Tab>

      <Tab title="Hono">
        ```ts theme={null}
        import { ai } from "./firstflow.server";

        app.post("/api/chat", async (c) => {
          const { userId, sessionId, message } = await c.req.json();
          const reply = await ai.messages.create({
            model: "claude-haiku-4-5",
            messages: [{ role: "user", content: message }],
            firstflow: { userId, sessionId },
          } as any);
          return c.json({ reply });
        });
        ```
      </Tab>
    </Tabs>

    Each call records a row in `firstflow_llm_calls`. See the
    [Server SDK](/server/overview) for traces, intent classification, and
    bring-your-own OpenAI-compatible clients.
  </Step>

  <Step title="Run it">
    Start your app. The welcome experience auto-fires after one second.

    ```bash theme={null}
    pnpm dev
    ```

    Run through it, then check Supabase `firstflow_flow_runs` has a completed row.
  </Step>
</Steps>

## Verify

A working setup shows three things:

1. The widget appears in the corner and advances through your experience.
2. `firstflow_flow_runs` gains a row per run (in-progress, then completed/dismissed).
3. If you wrapped an LLM client, `firstflow_llm_calls` gains a row per call.

If the widget never appears, confirm the experience `status` is `"active"` and the Supabase keys are set. If progress never persists, check the migrations are applied and [Row Level Security](/self-hosting/oauth-providers) allows the write.

## Next

<CardGroup cols={2}>
  <Card title="Set up Supabase" icon="database" href="/self-hosting/supabase-cloud">
    Create the project and apply the migrations in order.
  </Card>

  <Card title="Configuration" icon="sliders" href="/self-hosting/configuration">
    Every environment variable FirstFlow reads.
  </Card>

  <Card title="Identity & auth" icon="user" href="/self-hosting/auth-and-orgs">
    How `user.id` and anonymous visitors work.
  </Card>

  <Card title="Production" icon="shield-check" href="/self-hosting/production">
    RLS, key hygiene, and backups.
  </Card>
</CardGroup>
