> ## Documentation Index
> Fetch the complete documentation index at: https://bifrost-backport-semantic-cache-scope.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage

> Configure Bifrost storage backends in config.json - config_store, logs_store, vector_store, and object storage for logs

Bifrost persists two types of data - **config** (providers, virtual keys, governance rules) and **logs** (request/response records). Each has its own store. A **vector store** is required for semantic caching.

| Store | Purpose | Backends |
| - | - | - |
| `config_store` | Provider configs, virtual keys, governance rules | SQLite, PostgreSQL |
| `logs_store` | Request/response logs shown in UI | SQLite, PostgreSQL, ClickHouse + optional S3/GCS offload |
| `vector_store` | Semantic response caching | Weaviate, Redis, Valkey, Qdrant, Pinecone |

<Note>
  If you use PostgreSQL for any store, the target database must be **UTF8 encoded**. See [PostgreSQL UTF8 Requirement](/quickstart/gateway/setting-up#postgresql-utf8-requirement).
</Note>

***

## config\_store

<Note>
  When `config_store` is omitted, Bifrost creates a default SQLite config store in the app directory. Set `config_store.enabled` to `false` only when you want file-only configuration with no config-backed Web UI/API edits. See [Source of Truth & Reconciliation](/deployment-guides/config-json/source-of-truth).
</Note>

### SQLite (Default)

Simplest setup - no external database required. Bifrost stores configuration in a local SQLite file.

```json theme={null}
{
  "config_store": {
    "enabled": true,
    "type": "sqlite",
    "config": {
      "path": "./config.db"
    }
  }
}
```

| Field | Description |
| - | - |
| `config.path` | Path to the SQLite file (relative to app-dir, or absolute) |

### PostgreSQL

Production-grade storage suitable for high-availability and high-throughput deployments.

```json theme={null}
{
  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "max_idle_conns": 5,
      "max_open_conns": 50
    }
  }
}
```

| Field | Default | Description |
| - | - | - |
| `host` | - | PostgreSQL host (supports `env.` prefix) |
| `port` | - | PostgreSQL port (as string) |
| `user` | - | Database user (supports `env.` prefix) |
| `password` | - | Database password (supports `env.` prefix). Mutually exclusive with `password_command`; configure exactly one password source. |
| `password_command` | - | Command executed without a shell to produce the database password on stdout for each new physical connection. Mutually exclusive with `password`; runtime validation rejects configs that set both fields. Put only the executable path or name in `command`; pass arguments through `args`. |
| `db_name` | - | Database name |
| `ssl_mode` | - | `"disable"`, `"require"`, `"verify-ca"`, `"verify-full"` |
| `max_idle_conns` | `5` | Maximum idle connections in the pool |
| `max_open_conns` | `50` | Maximum open connections to the database |
| `conn_max_lifetime` | - | Maximum lifetime for physical database connections, as a Go duration string such as `"10m"`. |

Use `password_command` for short-lived database credentials such as AWS RDS IAM auth tokens:

```json theme={null}
{
  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "your-rds-endpoint.us-east-1.rds.amazonaws.com",
      "port": "5432",
      "user": "bifrost",
      "password_command": {
        "command": "aws",
        "args": [
          "rds",
          "generate-db-auth-token",
          "--hostname",
          "your-rds-endpoint.us-east-1.rds.amazonaws.com",
          "--port",
          "5432",
          "--region",
          "us-east-1",
          "--username",
          "bifrost"
        ],
        "timeout": "10s"
      },
      "db_name": "bifrost",
      "ssl_mode": "require",
      "conn_max_lifetime": "10m"
    }
  }
}
```

### Disabled (file-only mode)

Use this when you want Bifrost to read all configuration from `config.json` only - no configuration database and no config-backed Web UI/API edits.

```json theme={null}
{
  "config_store": {
    "enabled": false
  }
}
```

This is the recommended setup for [multinode OSS deployments](/deployment-guides/how-to/multinode) where a shared `config.json` is the single source of truth.

***

## logs\_store

Use `client.hidden_request_types` to hide selected request types from dashboard and log API reads while continuing to store their logs. The same setting is editable in the UI under **Logs Settings**. See [Hiding request types from the dashboard](/architecture/framework/log-store#hiding-request-types-from-the-dashboard) for configuration examples and behavior.

### SQLite

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "sqlite",
    "config": {
      "path": "./logs.db"
    }
  }
}
```

### PostgreSQL

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "max_idle_conns": 10,
      "max_open_conns": 100
    }
  }
}
```

`logs_store` supports the same dynamic PostgreSQL credential fields as `config_store`:

| Field | Default | Description |
| - | - | - |
| `password` | - | Database password (supports `env.` prefix). Mutually exclusive with `password_command`; configure exactly one password source. |
| `password_command` | - | Command executed without a shell to produce the database password on stdout for each new physical connection. Mutually exclusive with `password`; runtime validation rejects configs that set both fields. Put only the executable path or name in `command`; pass arguments through `args`. |
| `conn_max_lifetime` | - | Maximum lifetime for physical database connections, as a Go duration string such as `"10m"`. |

For high log volumes, increase `max_open_conns`:

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "max_idle_conns": 10,
      "max_open_conns": 200
    },
    "retention_days": 90
  }
}
```

### ClickHouse

A column-oriented backend built for high-volume log ingestion and fast analytical queries. Best suited for large-scale deployments where log throughput and dashboard query performance on big time ranges matter more than operational simplicity.

<Note>
  ClickHouse is a **`logs_store`-only** backend. The `config_store` supports only `sqlite` and `postgres` — pair a ClickHouse logs store with a SQLite or PostgreSQL config store (see [Mixed Backend Examples](#mixed-backend-examples)).
</Note>

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "clickhouse",
    "retention_days": 30,
    "config": {
      "host": "env.CLICKHOUSE_HOST",
      "port": "9000",
      "database": "bifrost",
      "username": "env.CLICKHOUSE_USER",
      "password": "env.CLICKHOUSE_PASSWORD"
    }
  }
}
```

`host` is the only required field; the rest have sensible defaults.

| Field | Default | Description |
| - | - | - |
| `host` | - | **Required.** ClickHouse host (supports `env.` prefix) |
| `port` | protocol-based | Port as a string. Defaults by protocol: native `9000` (`9440` with TLS), http `8123` (`8443` with TLS) |
| `database` | `default` | Database name (supports `env.` prefix) |
| `username` | - | ClickHouse user (supports `env.` prefix) |
| `password` | - | ClickHouse password (supports `env.` prefix) |
| `protocol` | `native` | Wire protocol: `"native"` or `"http"` |
| `secure` | `false` | Enable TLS |
| `dial_timeout` | `10000` | Connection dial timeout in **milliseconds** |
| `cluster` | - | Optional cluster name. When set, DDL runs `ON CLUSTER` with replicated table engines for a clustered ClickHouse deployment |

**TLS + HTTP protocol** against a managed ClickHouse (e.g. ClickHouse Cloud):

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "clickhouse",
    "retention_days": 30,
    "config": {
      "host": "env.CLICKHOUSE_HOST",
      "port": "8443",
      "database": "bifrost",
      "username": "env.CLICKHOUSE_USER",
      "password": "env.CLICKHOUSE_PASSWORD",
      "protocol": "http",
      "secure": true
    }
  }
}
```

**Clustered ClickHouse** — set `cluster` so tables are created with replicated engines across the cluster:

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "clickhouse",
    "config": {
      "host": "env.CLICKHOUSE_HOST",
      "database": "bifrost",
      "username": "env.CLICKHOUSE_USER",
      "password": "env.CLICKHOUSE_PASSWORD",
      "cluster": "my_cluster"
    }
  }
}
```

<Note>
  With ClickHouse, `retention_days` is enforced by a native table **TTL** rather than a background delete job. The TTL is reconciled on every startup, so changing `retention_days` later updates the existing `logs`, `mcp_tool_logs` and `webhook_deliveries` tables (metadata only; expired rows are dropped by ClickHouse's regular TTL merges). Setting it to `0` (or omitting it) leaves any existing TTL untouched and creates new tables without one, so ClickHouse itself never expires rows unless you manage the TTL yourself. Background cleanup is controlled separately by `client_config.log_retention_days`; on ClickHouse each run is a single lightweight `DELETE FROM ... WHERE created_at < cutoff`, as are the periodic sweeps of stale `processing` rows, so setting both is safe. Lightweight deletes require ClickHouse 24.4 or newer. `matview_refresh_interval` and `matview_refresh_timeout` do not apply - they are PostgreSQL-only settings for materialized views.
</Note>

### Disabled

```json theme={null}
{
  "logs_store": {
    "enabled": false
  }
}
```

### Log Retention

Set `retention_days` to automatically purge old log entries. `0` disables retention-based cleanup.

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": { "...": "..." },
    "retention_days": 90
  }
}
```

### Materialized View Refresh Interval (PostgreSQL only)

The PostgreSQL logs store backs the dashboard's stats and histograms with materialized views, refreshed in the background. The default cadence is **1 minute**, which keeps dashboard data near real-time but issues a `REFRESH MATERIALIZED VIEW CONCURRENTLY` every minute — an expensive operation that can be too aggressive on smaller or CPU-constrained database instances.

Set `matview_refresh_interval` (Go duration string) to slow down refreshes when near-real-time accuracy isn't critical:

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "matview_refresh_interval": "5m"
    }
  }
}
```

| Field | Default | Description |
| - | - | - |
| `matview_refresh_interval` | `"1m"` | How often to refresh dashboard materialized views. Accepts any Go duration string (`"1m"`, `"5m"`, `"1h"`); positive values below `5s` are clamped up to `5s`. Set `"off"` or a zero duration (`"0s"`) to disable matview maintenance entirely. |

**Notes**

* Refreshes are already **activity-gated**: when no INSERT/UPDATE/DELETE has hit the `logs` table since the last refresh, the scheduled tick short-circuits without touching the views. So idle clusters don't pay for the configured cadence — they only pay when there's actual log activity.
* Dashboard freshness lag will be **at most** the configured interval. Stats and histograms over the last 24 hours come straight from the raw `logs` table (no matview), so short-window dashboards stay real-time regardless of this setting.
* A 10-minute safety-net refresh runs even on totally idle clusters so the rolling 30-day filter dropdown window evicts aged-out values.

**When to raise it:**

* Your database instance is CPU-constrained and matview refreshes are showing up as a hot consumer.
* Your team mostly looks at multi-day trends, not minute-by-minute dashboards.

**When to leave it at the default:**

* The database has consistent CPU headroom.
* Operators rely on near-real-time dashboards (e.g. live incident triage).

**When to turn it off:**

* You don't use the Bifrost dashboard (e.g. Bifrost runs headless behind your own observability stack). With `"off"`, the views are neither created nor refreshed, and any dashboard query transparently uses the raw tables.

### Object Storage for Logs

Offload LLM request/response logs and MCP tool logs from the database to S3 or GCS. The database retains lightweight index records and fetches full payloads on demand. For MCP logs, the full tool log is stored in object storage and the database keeps dashboard/table fields plus a 200-character input preview.

#### AWS S3

**Required IAM permissions**

The IAM user or role needs the following permissions on your bucket:

```json theme={null}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BucketAccess",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::bifrost-logs"
    },
    {
      "Sid": "ObjectAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:PutObjectTagging",
        "s3:GetObjectTagging"
      ],
      "Resource": "arn:aws:s3:::bifrost-logs/*"
    }
  ]
}
```

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": { "...": "..." },
    "object_storage": {
      "type": "s3",
      "bucket": "env.S3_BUCKET",
      "prefix": "bifrost",
      "compress": true,
      "region": "us-east-1",
      "access_key_id": "env.S3_ACCESS_KEY_ID",
      "secret_access_key": "env.S3_SECRET_ACCESS_KEY"
    }
  }
}
```

**IAM role (instance profile / IRSA)** - omit `access_key_id` and `secret_access_key`:

```json theme={null}
{
  "object_storage": {
    "type": "s3",
    "bucket": "bifrost-logs",
    "region": "us-east-1",
    "compress": true,
    "role_arn": "arn:aws:iam::123456789012:role/BifrostS3Role"
  }
}
```

| Field | Description |
| - | - |
| `bucket` | S3 bucket name (supports `env.` prefix) |
| `prefix` | Key prefix for stored objects (default: `"bifrost"`) |
| `compress` | Enable gzip compression (default: `false`) |
| `region` | AWS region |
| `access_key_id` | AWS access key ID (omit for default credential chain) |
| `secret_access_key` | AWS secret access key |
| `session_token` | STS temporary credentials session token |
| `role_arn` | IAM role ARN for STS AssumeRole |
| `endpoint` | Custom endpoint for MinIO / Cloudflare R2 |
| `force_path_style` | Use path-style URLs (required for MinIO, default: `false`) |

<Accordion title="Using KMS to encrypt your bucket?">
  1. Attach this IAM policy to whichever AWS principal Bifrost authenticates as: the IAM user behind `access_key_id`/`secret_access_key`, or the IAM role behind `role_arn`:

  ```json theme={null}
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "KMSAccess",
        "Effect": "Allow",
        "Action": ["kms:GenerateDataKey", "kms:Decrypt"],
        "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
      }
    ]
  }
  ```

  2. If you're using a **customer-managed key** (not the AWS-managed `aws/s3` key), it also needs permission granted separately on the **key's own policy** (in the KMS console). Add that same IAM user or role ARN there too ([AWS guide](https://repost.aws/knowledge-center/s3-bucket-access-default-encryption)). AWS-managed keys don't allow their key policy to be edited, so this step doesn't apply if you're using the default `aws/s3` key.

  <Info>
    Default encryption applies KMS without requiring encryption headers from the uploader. Bifrost's IAM identity still needs the KMS permissions above regardless of encryption mode. If instead your bucket policy denies uploads that don't include the encryption header, note that Bifrost does not send that header, so uploads will fail under that policy.
  </Info>
</Accordion>

#### Google Cloud Storage

```json theme={null}
{
  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": { "...": "..." },
    "object_storage": {
      "type": "gcs",
      "bucket": "bifrost-logs",
      "prefix": "bifrost",
      "compress": true,
      "project_id": "env.GCP_PROJECT_ID",
      "credentials_json": "env.GCS_CREDENTIALS_JSON"
    }
  }
}
```

Omit `credentials_json` to use Application Default Credentials (Workload Identity, GCE metadata, `gcloud auth`).

| Field | Description |
| - | - |
| `project_id` | GCP project ID (supports `env.` prefix) |
| `credentials_json` | Service account JSON or path - omit for ADC |

#### MinIO (Self-Hosted)

```json theme={null}
{
  "object_storage": {
    "type": "s3",
    "bucket": "bifrost-logs",
    "prefix": "bifrost",
    "compress": false,
    "region": "us-east-1",
    "endpoint": "http://minio.internal:9000",
    "access_key_id": "env.MINIO_ACCESS_KEY",
    "secret_access_key": "env.MINIO_SECRET_KEY",
    "force_path_style": true
  }
}
```

***

## vector\_store

A vector store is required for [semantic caching](/features/semantic-caching). Choose from Weaviate, Redis/Valkey, Qdrant, or Pinecone.

### Weaviate

```json theme={null}
{
  "vector_store": {
    "enabled": true,
    "type": "weaviate",
    "config": {
      "scheme": "http",
      "host": "localhost:8080",
      "api_key": "env.WEAVIATE_API_KEY",
      "grpc_config": {
        "host": "localhost:50051",
        "secured": false
      }
    }
  }
}
```

| Field | Required | Description |
| - | - | - |
| `scheme` | Yes | `"http"` or `"https"` |
| `host` | Yes | Weaviate server host and port |
| `api_key` | No | Weaviate API key (supports `env.` prefix) |
| `grpc_config.host` | No | gRPC host for faster vector operations |
| `grpc_config.secured` | No | Use TLS for gRPC connection |

### Redis / Valkey

```json theme={null}
{
  "vector_store": {
    "enabled": true,
    "type": "redis",
    "config": {
      "addr": "env.REDIS_ADDR",
      "password": "env.REDIS_PASSWORD",
      "db": 0,
      "use_tls": false
    }
  }
}
```

**AWS MemoryDB (cluster mode):**

```json theme={null}
{
  "vector_store": {
    "enabled": true,
    "type": "redis",
    "config": {
      "addr": "env.MEMORYDB_ENDPOINT",
      "password": "env.MEMORYDB_PASSWORD",
      "use_tls": true,
      "cluster_mode": true
    }
  }
}
```

| Field | Default | Description |
| - | - | - |
| `addr` | - | Redis/Valkey address `host:port` (supports `env.` prefix) |
| `password` | - | Redis AUTH password (supports `env.` prefix) |
| `db` | `0` | Redis database number |
| `use_tls` | `false` | Enable TLS |
| `cluster_mode` | `false` | Enable cluster mode (required for MemoryDB; `db` must be `0`) |
| `pool_size` | - | Maximum socket connections |

### Qdrant

```json theme={null}
{
  "vector_store": {
    "enabled": true,
    "type": "qdrant",
    "config": {
      "host": "env.QDRANT_HOST",
      "port": 6334,
      "api_key": "env.QDRANT_API_KEY",
      "use_tls": false
    }
  }
}
```

| Field | Default | Description |
| - | - | - |
| `host` | - | Qdrant server host (supports `env.` prefix) |
| `port` | `6334` | gRPC port |
| `api_key` | - | API key (supports `env.` prefix) |
| `use_tls` | `false` | Enable TLS |

### Pinecone

Pinecone is external-only.

```json theme={null}
{
  "vector_store": {
    "enabled": true,
    "type": "pinecone",
    "config": {
      "api_key": "env.PINECONE_API_KEY",
      "index_host": "env.PINECONE_INDEX_HOST"
    }
  }
}
```

| Field | Description |
| - | - |
| `api_key` | Pinecone API key (supports `env.` prefix) |
| `index_host` | Index host from Pinecone console (e.g. `your-index.svc.us-east1-gcp.pinecone.io`) |

***

## Mixed Backend Examples

Each store is configured independently, so you can run the config store and logs store on **different backends — or even different database instances**. This is useful when config and logs have different scaling, cost, or retention profiles.

<Note>
  In `config.json` each store carries its own `config` block, so the two stores can point at entirely separate hosts. The Helm chart shares one PostgreSQL connection across both stores by default; set `storage.logsStore.postgres.enabled: true` to point the logs store at a separate PostgreSQL instance. See [Separate PostgreSQL for Logs](/deployment-guides/helm/storage#separate-postgresql-for-logs).
</Note>

### Config on PostgreSQL #1, Logs on PostgreSQL #2

Keep configuration on a small, highly-available Postgres while sending high-volume logs to a separate Postgres instance sized for write throughput — so log traffic never competes with config reads:

```json theme={null}
{
  "$schema": "https://www.getbifrost.ai/schema",
  "encryption_key": "env.BIFROST_ENCRYPTION_KEY",

  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_CONFIG_HOST",
      "port": "5432",
      "user": "env.PG_CONFIG_USER",
      "password": "env.PG_CONFIG_PASSWORD",
      "db_name": "bifrost_config",
      "ssl_mode": "require",
      "max_idle_conns": 5,
      "max_open_conns": 50
    }
  },

  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_LOGS_HOST",
      "port": "5432",
      "user": "env.PG_LOGS_USER",
      "password": "env.PG_LOGS_PASSWORD",
      "db_name": "bifrost_logs",
      "ssl_mode": "require",
      "max_idle_conns": 10,
      "max_open_conns": 200
    },
    "retention_days": 90
  }
}
```

### Config on PostgreSQL, Logs on ClickHouse

Run configuration on PostgreSQL (transactional, backs the Web UI) while sending logs to ClickHouse for high-volume ingestion and fast analytics. ClickHouse is a logs-store-only backend, so this pairing is the recommended shape for analytics-heavy deployments:

```json theme={null}
{
  "$schema": "https://www.getbifrost.ai/schema",
  "encryption_key": "env.BIFROST_ENCRYPTION_KEY",

  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require"
    }
  },

  "logs_store": {
    "enabled": true,
    "type": "clickhouse",
    "config": {
      "host": "env.CLICKHOUSE_HOST",
      "port": "9000",
      "database": "bifrost",
      "username": "env.CLICKHOUSE_USER",
      "password": "env.CLICKHOUSE_PASSWORD"
    },
    "retention_days": 30
  }
}
```

***

## Full Storage Example

```json theme={null}
{
  "$schema": "https://www.getbifrost.ai/schema",
  "encryption_key": "env.BIFROST_ENCRYPTION_KEY",

  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "max_idle_conns": 5,
      "max_open_conns": 50
    }
  },

  "logs_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "max_idle_conns": 10,
      "max_open_conns": 100
    },
    "retention_days": 90,
    "object_storage": {
      "type": "s3",
      "bucket": "env.S3_BUCKET",
      "region": "us-east-1",
      "compress": true,
      "access_key_id": "env.S3_ACCESS_KEY_ID",
      "secret_access_key": "env.S3_SECRET_ACCESS_KEY"
    }
  },

  "vector_store": {
    "enabled": true,
    "type": "weaviate",
    "config": {
      "scheme": "http",
      "host": "weaviate:8080"
    }
  }
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.