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

# What gets recorded

> The four Supabase tables the server SDK writes to, and the columns on each so you know exactly where your observability data lives.

Everything the server SDK captures lands in four tables in your own Supabase. There is no hidden store and no external service. This page is the map.

## The tables

| Table                             | One row per       | Written by                                              |
| --------------------------------- | ----------------- | ------------------------------------------------------- |
| `firstflow_llm_calls`             | LLM API call      | every wrapped call                                      |
| `firstflow_conversation_messages` | message turn      | wrapped calls that carry a `conversationId`/`sessionId` |
| `firstflow_conversations`         | conversation      | upserted as messages accumulate                         |
| `firstflow_traces`                | logical operation | [`ff.withTrace()`](/server/traces)                      |

## `firstflow_llm_calls`

The core observability row. Key columns:

* `user_id`, `provider` (`openai`/`anthropic`), `model`, `operation` (`chat`/`embedding`)
* `input_tokens`, `output_tokens`, `total_tokens`, `cache_read_tokens`, `cache_write_tokens`
* `input_cost_usd`, `output_cost_usd`, `total_cost_usd` (computed from model pricing)
* `latency_ms`, `status` (`success`/`error`), `error_message`, `http_status`
* `trace_id`, `span_id`, `metadata` (jsonb)
* `prompt_messages`, `completion_text` only when `captureContent: true`

## `firstflow_conversation_messages`

Recorded when a call carries a `conversationId`/`sessionId` **and** a `userId`: the prompting user turn and the assistant reply, sharing the same `trace_id`/`span_id`.

* `conversation_id`, `user_id`, `role`, `content`
* `model`, `provider`, token and cost columns, `latency_ms`, `finish_reason`, `http_status`
* `is_error`, `error`, `properties` (jsonb)

## `firstflow_conversations`

One row per conversation, upserted with running aggregates and (optional) analysis:

* `id` (the conversation id), `user_id`, `message_count`
* `total_input_tokens`, `total_output_tokens`, `total_cost_usd`, `error_count`, `last_message_at`
* `state` (jsonb) holds the active intent; `intent`, `outcome`, `tags`, `notes`
* `sentiment`, `sentiment_score`, `topics`, and `eval_*` columns

<Note>
  The `intent`, `sentiment`, and related columns require migration
  `0007_conversation_signals.sql`. If a classifier runs but these never update,
  apply that migration. See [Set up Supabase](/self-hosting/supabase-cloud).
</Note>

## `firstflow_traces`

The parent row for a [`withTrace`](/server/traces) operation: `id`, `user_id`, `name`, `input_state`, `output_state`, `latency_ms`, `is_error`, `error`, `metadata`, `started_at`, `ended_at`.

## Custom persistence

The tables above are what the shipped Supabase adapter writes. The SDK records through a `FirstflowLLMPersistence` interface, so you can target another store by implementing it (`recordCall`, optional `recordTrace`/`recordMessage`/`tagConversation`/`recordSentiment`/`recordIntent`, …). See [Configuration](/server/configuration).

## Next

Set conversation [outcomes & signals](/server/outcomes), or classify [intent & sentiment](/server/analytics).
