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

# Server SDK overview

> @firstflow/runtime-server wraps your LLM client to record tokens, cost, latency, and conversations to your Supabase and classifies intent. No FirstFlow service involved.

`@firstflow/runtime-server` is the optional backend half of FirstFlow. It wraps your existing LLM client so every call's tokens, cost, latency, and (opt-in) content are recorded to your Supabase, groups calls into conversations and traces, and runs an intent classifier. It writes directly to your database there is no FirstFlow API in the path.

You consume it by cloning the repo and building your app inside its workspace (it's not on npm) see [Self-hosting → Quickstart](/self-hosting/quickstart). Your app references it with `"@firstflow/runtime-server": "workspace:*"`.

## Wrap and go

```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.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // server-only, bypasses RLS
);

const ff = new Firstflow({
  persistence: createSupabaseLLMPersistence(supabase),
  captureContent: true,
});

const ai = ff.wrap(new Anthropic());

await ai.messages.create({
  model: "claude-haiku-4-5",
  messages: [{ role: "user", content: userInput }],
  firstflow: { userId, sessionId },
} as any);
```

That call records one row in `firstflow_llm_calls` and, because a `sessionId` is present, two messages in `firstflow_conversation_messages`.

## What it provides

| Surface                                                | Purpose                                                   |
| ------------------------------------------------------ | --------------------------------------------------------- |
| [`ff.wrap(client)`](/server/wrap-llm-client)           | Proxy an OpenAI or Anthropic client; capture every call   |
| `createAIClient` (`/aiclient`)                         | Pre-wrapped OpenAI-compatible client for Ollama/vLLM/etc. |
| [`ff.withTrace(fn)`](/server/traces)                   | Group multiple calls under one trace                      |
| [`ff.classifyIntent(...)`](/server/analytics)          | LLM intent classification with hysteresis                 |
| [`ff.classifySentiment(...)`](/server/analytics)       | Lexical sentiment, no LLM call                            |
| [`ff.signal` / `ff.tagConversation`](/server/outcomes) | Set intent, outcome, tags, notes on a conversation        |
| `signUserToken` / `verifyUserToken`                    | HMAC-signed identity tokens                               |

## Runtime support

Works in any runtime with Web Crypto: Node 18+, Bun, Deno, Cloudflare Workers, and Vercel Edge. Provider detection is by shape no hard dependency on the OpenAI or Anthropic SDK. Apps that use Anthropic never load `openai`, and vice versa.

## Where data goes

Everything lands in your Supabase: `firstflow_llm_calls`, `firstflow_traces`, `firstflow_conversations`, and `firstflow_conversation_messages`. See [What gets recorded](/server/observe).

## Next

<CardGroup cols={2}>
  <Card title="Wrap your LLM client" icon="plug" href="/server/wrap-llm-client">
    Providers, call metadata, scope, and bring-your-own endpoints.
  </Card>

  <Card title="Traces & spans" icon="diagram-project" href="/server/traces">
    Group multi-call operations.
  </Card>

  <Card title="Intent & sentiment" icon="brain" href="/server/analytics">
    Classify conversations.
  </Card>

  <Card title="Configuration" icon="sliders" href="/server/configuration">
    Keys, persistence, and content capture.
  </Card>
</CardGroup>
