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

# Configuration

> How to construct Firstflow: persistence, content capture, default metadata, the keys it needs, and signed identity tokens.

The server SDK is configured in code through the `Firstflow` constructor plus a couple of environment values. There is no FirstFlow service, so there are no service URLs or API keys to point at.

## Constructing `Firstflow`

```ts theme={null}
const ff = new Firstflow({
  persistence: createSupabaseLLMPersistence(supabase),
  captureContent: false,
  defaultMetadata: { app: "web", env: process.env.NODE_ENV },
});
```

<ParamField path="persistence" type="FirstflowLLMPersistence" required>
  Where rows are written. Pass `createSupabaseLLMPersistence(supabase)` from
  `@firstflow/runtime-server/supabase`, or implement the interface for another
  store.
</ParamField>

<ParamField path="captureContent" type="boolean" default="false">
  Store prompt and completion text in `firstflow_conversation_messages`. Off by
  default only metadata (tokens, cost, latency, status) is recorded. Enable it
  deliberately and match your RLS and retention to your privacy posture.
</ParamField>

<ParamField path="defaultMetadata" type="Record<string, unknown>">
  Tags merged into every recorded call's `metadata`. Per-call `firstflow.metadata`
  is merged on top.
</ParamField>

## Environment

The SDK itself needs only your Supabase project URL and **service-role** key (for the adapter):

```bash theme={null}
SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...     # server-only, bypasses RLS
```

<Warning>
  `SUPABASE_SERVICE_ROLE_KEY` bypasses Row Level Security. Keep it server-only,
  never expose it to the browser, and never commit it.
</Warning>

**No AI key is read by FirstFlow.** The key belongs to the LLM client you build and hand to `ff.wrap(...)` / `createAIClient(...)`. Provide it however you like the provider SDK's default variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`), your own env name passed explicitly, or `createAIClient({ baseURL, apiKey })` for an OpenAI-compatible endpoint. See the [provider tabs in self-hosting Configuration](/self-hosting/configuration) and [Wrap your LLM client](/server/wrap-llm-client).

## Custom persistence

Target a non-Supabase store by implementing `FirstflowLLMPersistence`:

```ts theme={null}
interface FirstflowLLMPersistence {
  recordCall(call): Promise<void>;
  recordTrace?(trace): Promise<void>;
  recordMessage?(message): Promise<void>;
  tagConversation?(input): Promise<void>;
  recordSentiment?(input): Promise<void>;
  recordIntent?(input): Promise<void>;
  getRecentMessages?(conversationId, limit): Promise<MessagePeek[]>;
  getConversationIntentState?(conversationId): Promise<...>;
}
```

Only `recordCall` is required; optional methods unlock messages, traces, signals, and the classifier's read path.

## Signed identity tokens

When you proxy SDK writes through your own API, `signUserToken` mints a tamper-proof identity payload your backend can verify:

```ts theme={null}
import { signUserToken, verifyUserToken } from "@firstflow/runtime-server";

const token = await signUserToken({
  userId: session.user.id,
  traits: { plan: "pro" },
  secret: process.env.FIRSTFLOW_SECRET!,
  expiresIn: 3600,
});

const claims = await verifyUserToken(token, process.env.FIRSTFLOW_SECRET!);
```

Standard HS256 JWTs any JWT library can verify them.

## Next

Start with [wrapping your client](/server/wrap-llm-client), or see [what gets recorded](/server/observe).
