> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitfab.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Database Branching for Replay

> Replay traces against the database state that existed when they were captured

By default, replay re-runs a historical trace's inputs through your current code, but your function still talks to your **current** database. That breaks fidelity for anything that reads stored state: a refund decision over a since-cancelled order, a retrieval step over last week's rows, a tool call gated by a row that no longer exists.

**Database branching** pins each trace to the database state at capture time. On replay, Bitfab provisions an ephemeral branch of your database at that exact moment and hands your replayed function a connection URL pointing at it. The replay reads the world as it was when the trace happened.

<Note>
  Available in the **TypeScript, Python, and Ruby** SDKs (the ones with replay + `ReplayEnvironment`). The Go SDK has no replay, so database branching does not apply.
</Note>

## How it works

Capture is **automatic**: every root trace already records the wall-clock instant it ran, with no SDK configuration. So there's nothing to turn on; setup is just two steps:

1. **Connect your database once** in the Bitfab dashboard. Your source database can be any Postgres; Bitfab provisions a managed, branchable copy from it.
2. **Wire replay** to read the per-trace branch URL through a `ReplayEnvironment`.

At replay time, for each item Bitfab branches the managed copy to the trace's captured instant and returns a short-lived connection URL. Your replayed function connects to that branch instead of production. The branch is deleted after the run.

## 1. Connect your database

In the Bitfab dashboard, open **Integrations → Database** and paste your Postgres connection string. Bitfab provisions a branchable managed copy from it; discovery and engine setup take a few minutes, after which the Database section shows **Connected**.

Your source database can be any Postgres. You do **not** set any `NEON_*` environment variables (those are Bitfab-side server configuration). You only provide your source connection string in the dashboard. The connection string is encrypted at rest, and replay branches are isolated, ephemeral copies; replay writes never touch your source database.

## 2. Wire replay to the branch

Construct one `ReplayEnvironment`, pass it to the replay call, and connect through its branch URL inside the replayed function. The construction call, the replay option, and the accessors differ slightly per SDK.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Bitfab, ReplayEnvironment } from "@bitfab/sdk"

  const bitfab = new Bitfab({ apiKey: process.env.BITFAB_API_KEY })
  const env = new ReplayEnvironment()
  const pipeline = bitfab.getFunction("checkout-agent")

  // The replayed function connects to env.databaseUrl when a per-trace branch
  // is available, and to the normal env var otherwise.
  const runCheckout = pipeline.withSpan(
    { name: "CheckoutAgent", type: "agent" },
    async (orderId: string) => {
      const url = env.active ? env.databaseUrl : process.env.DATABASE_URL
      const db = makeDbClient(url) // your normal client factory
      const order = await db.orders.find(orderId)
      return decideRefund(order)
    },
  )

  const result = await bitfab.replay("checkout-agent", runCheckout, {
    limit: 10,
    environment: env,
  })
  console.log(result.testRunUrl)
  ```

  ```python Python theme={null}
  import os
  from bitfab import Bitfab, ReplayEnvironment

  bitfab = Bitfab(api_key=os.environ["BITFAB_API_KEY"])
  env = ReplayEnvironment()
  pipeline = bitfab.get_function("checkout-agent")

  @pipeline.span(name="CheckoutAgent", type="agent")
  def run_checkout(order_id: str):
      url = env.database_url if env.active else os.environ["DATABASE_URL"]
      db = make_db_client(url)  # your normal client factory
      order = db.orders.find(order_id)
      return decide_refund(order)

  result = bitfab.replay(run_checkout, limit=10, environment=env)
  print(result["testRunUrl"])
  ```

  ```ruby Ruby theme={null}
  require "bitfab"

  Bitfab.configure(api_key: ENV["BITFAB_API_KEY"])
  client = Bitfab.client
  env = Bitfab::ReplayEnvironment.new

  class CheckoutAgent
    include Bitfab::Traceable
    bitfab_function "checkout-agent"

    def initialize(env) = @env = env

    bitfab_span :run_checkout, name: "CheckoutAgent", type: "agent"
    def run_checkout(order_id)
      url = @env.active? ? @env.database_url : ENV["DATABASE_URL"]
      db = make_db_client(url) # your normal client factory
      decide_refund(db.orders.find(order_id))
    end
  end

  agent = CheckoutAgent.new(env)
  result = client.replay(
    agent, :run_checkout,
    trace_function_key: "checkout-agent",
    limit: 10,
    environment: env,
  )
  puts result[:testRunUrl]
  ```
</CodeGroup>

### Reading the environment

The environment is active only inside a replay item that has a resolved branch. Always check that first:

* On the **live request path**, the environment is inactive: your function keeps using its normal `DATABASE_URL`. The same code works in production and in replay.
* For **traces captured before the SDK version that added always-on snapshot capture**, the environment is inactive (no snapshot ref), so the item replays against your normal database.
* Reading the branch URL while inactive **throws**. Gate every read on the active flag.

|              | TypeScript        | Python             | Ruby               |
| ------------ | ----------------- | ------------------ | ------------------ |
| Active flag  | `env.active`      | `env.active`       | `env.active?`      |
| Branch URL   | `env.databaseUrl` | `env.database_url` | `env.database_url` |
| Expiry       | `env.expiresAt`   | `env.expires_at`   | `env.expires_at`   |
| Read-only    | `env.readOnly`    | `env.read_only`    | `env.read_only`    |
| Source trace | `env.traceId`     | `env.trace_id`     | `env.trace_id`     |

### Resolve the connection per call, not at import

The most common reason a wired replay still hits production is a database client created **once at module import**: a module-level pool/engine bound to `DATABASE_URL` captures that value before any replay context exists. Refactor so the connection string is resolved **per call** (or per replay item) and your replayed function can build, or be handed, a client from the environment's branch URL. If you can't move off an import-time pool, run replay in a process where `DATABASE_URL` is set to the branch URL before the module loads.

## Verifying it works

Capture is automatic, but a trace only carries a snapshot ref if it was recorded by an SDK version with always-on capture, so smoke-test with a **recently captured** trace:

1. Run the instrumented function once so a new trace lands.
2. Replay that trace and confirm, inside the function, that the environment is active and its branch URL's host differs from your normal `DATABASE_URL`.
3. Open the test run URL from the replay result to inspect the experiment.

### Bitfab tracks whether the branch was actually used

Each replayed trace records whether your code actually obtained the branch URL during that item (reading `databaseUrl` / `database_url` or calling `snapshot()` counts; checking the active flag alone does not). The experiment data for the trace then shows one of three states:

* **Used**: a branch was provisioned and your function took its URL.
* **Provisioned but never read**: a branch was ready, but the function never asked for the URL. This usually means the replay silently hit your live database; the most common cause is a connection pool created at module import (see "Resolve the connection per call, not at import" above).
* **No branch**: the item had no snapshot to branch from, or branch provisioning failed, and the function ran against its normal database.

## Optional: pin the provider (TypeScript)

Capture works with no configuration. In TypeScript you *may* pass a provider to pin it at capture time; it is not required, and the provider is otherwise resolved at replay time:

```typescript theme={null}
const bitfab = new Bitfab({
  apiKey: process.env.BITFAB_API_KEY,
  dbSnapshot: { provider: "neon" },
})
```

Python and Ruby have no equivalent option; capture is fully automatic.

## Limitations

* **TypeScript, Python, and Ruby** only (Go has no replay).
* **Postgres only.**
* Branch leases are **short-lived** (a few minutes) and created fresh per replay item. Replay completes well within that window; the lease is released and the branch deleted afterward.
* The branch reflects the source database's state at the captured instant, **bounded by replication lag** (typically sub-second to a few seconds).
* Replay writes land only on the ephemeral branch and are discarded; they never propagate to your source database.
* Only traces captured by an SDK version with always-on snapshot capture can be branched. Older traces replay against your normal database.
