---
title: "Adapters"
description: "Built-in destination adapters — Bun SQLite, Turso, ClickHouse, and Analytics Engine."
---

> Documentation Index
> Fetch the complete documentation index at: https://logbun-docs.abshahin.workers.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Adapters

Destination adapters store queryable audit records. Reliability adapters own journals and DLQs. Pair a destination below with `FileReliabilityAdapter` or `CloudflareReliabilityAdapter` for durable mode.

`AuditLogger.query()` throws if the logger is shut down or not initialized. Built-in destination adapters throw from `query` / `bulkInsert` / `prune` if they are closed or not initialized — they do not return an empty page as if no rows existed.

Adapter `query` is newest-first (`ORDER BY id DESC`). `pagination.limit` is required on `IAdapter.query`. `nextCursor` is the last row's `id` when another page exists.

## BunSQLiteAdapter

```ts
import { BunSQLiteAdapter } from 'logbun/adapters/bun-sqlite';

const adapter = new BunSQLiteAdapter({
  path: '.logbun/audit.db',
  synchronous: 'FULL',     // FULL | NORMAL | OFF — default FULL
  busyTimeoutMs: 5_000,    // default 5000
});
```

| | |
|--|--|
| **Deps** | None (`bun:sqlite`) |
| **Best for** | Dev, single-instance |
| **Idempotency** | `INSERT OR IGNORE` on primary key `id` |
| **Journal** | `PRAGMA journal_mode = WAL` |
| **Prune** | Batched `DELETE … WHERE created_at < cutoff AND id IN (SELECT id … LIMIT 1000)`, up to 10_000 batches. Throws `prune_incomplete` if a full batch remains. `days` must be a finite number `>= 1`. |
| **Indexes** | `(tenant_id, created_at)`, `(tenant_id, id DESC)`, `(tenant_id, action, created_at)`, `(tenant_id, actor_id, created_at)`, `(tenant_id, entity_id)`, plus standalone action/actor/entity |
| **Integrity** | Columns `prev_hash`, `content_hash`; best-effort `ALTER TABLE … ADD COLUMN` for older DBs |

**Not multi-writer HA.** Multiple processes must not share one SQLite file as concurrent writers.

Invalid `synchronous` throws `BunSQLiteAdapter: synchronous must be FULL, NORMAL, or OFF`. Invalid `busyTimeoutMs` throws `BunSQLiteAdapter: busyTimeoutMs must be a finite integer >= 0`.

## TursoAdapter

```ts
import { TursoAdapter } from 'logbun/adapters/turso';

const adapter = new TursoAdapter({
  url: 'libsql://my-db.turso.io',
  authToken: process.env.TURSO_TOKEN!,
});
```

| | |
|--|--|
| **Peer** | `@libsql/client` `>=0.6.0` |
| **Best for** | Multi-tenant SaaS, edge, `database_per_tenant` |
| **Idempotency** | `INSERT OR IGNORE` |
| **Writes** | `client.batch(..., 'write')` |
| **Prune** | Same batched DELETE as SQLite. Throws `prune_incomplete` if `rowsAffected` is missing or a full batch remains after 10_000 × 1_000 rows. |
| **Indexes** | Same composite set as SQLite, including `(tenant_id, id DESC)` |
| **Integrity** | Same `prev_hash` / `content_hash` columns + best-effort migrate |

Config: `{ url: string; authToken: string }`.

## ClickHouseAdapter

```ts
import { ClickHouseAdapter } from 'logbun/adapters/clickhouse';

const adapter = new ClickHouseAdapter({
  url: 'http://localhost:8123',
  database: 'analytics',     // default 'default'
  username: 'default',
  password: process.env.CH_PASSWORD,
  retentionDays: 90,         // TTL on CREATE TABLE only; safe integer 1..365000
  queryFinal: true,          // default true — deduped reads
});
```

| | |
|--|--|
| **Peer** | `@clickhouse/client` `>=1.0.0` |
| **Best for** | High-volume analytics / audit warehouse |
| **Engine** | `ReplacingMergeTree` · `ORDER BY (tenant_id, id)` · `PARTITION BY toYYYYMM(created_at)` |
| **Idempotency** | Sorting-key dedup after merges; query uses `FINAL` by default (`queryFinal !== false`) |
| **Prune** | `ALTER TABLE … DROP PARTITION` for YYYYMM partitions strictly older than the cutoff month (UTC). The cutoff month itself is left to create-time TTL. Does not throw `prune_incomplete`. |
| **Timestamps** | ISO → `YYYY-MM-DD HH:mm:ss.SSS` UTC via `toClickHouseDateTime` |
| **Integrity** | Nullable `prev_hash` / `content_hash`; best-effort `ADD COLUMN IF NOT EXISTS` |

- `CREATE TABLE IF NOT EXISTS` does **not** migrate an existing MergeTree **engine**. Old installs need a manual migration to ReplacingMergeTree.
- `retentionDays` TTL is applied **only at CREATE TABLE time**. Changing the option later does not `ALTER` an existing table’s TTL.
- `tenant_id` is `String DEFAULT ''` (not nullable); empty round-trips as omitted `tenantId` on read (`''` → `undefined`).
- Prefer a **shared** ClickHouse database (not one DB per tenant).
- Invalid `retentionDays` throws `ClickHouseAdapter: retentionDays must be a safe integer in 1..365000`.

Exported helpers:

```ts
import {
  toClickHouseDateTime,
  fromClickHouseDateTime,
} from 'logbun/adapters/clickhouse';
```

## CloudflareAnalyticsEngineAdapter — Workers Analytics Engine (destination)

**Workers-only destination.** For the `analytics_engine_datasets` binding. This
is **not** a reliability adapter and does **not** use D1/R2/KV or Durable Object
SQLite — that is `CloudflareReliabilityAdapter` (Durable Object journal/DLQ).
See the [Cloudflare guide](/guides/cloudflare) for how they pair. Use AE for
the warehouse, use the DO adapter for durability.

```ts
import { CloudflareAnalyticsEngineAdapter } from 'logbun/adapters/cloudflare-analytics-engine';

const adapter = new CloudflareAnalyticsEngineAdapter({
  binding: env.AUDIT_AE,    // writeDataPoint binding — required for bulkInsert
  dataset: 'logbun_audit',  // SQL dataset — required for query
  accountId: env.ACCOUNT_ID,   // 32-char account id — required for query
  apiToken: env.AE_API_TOKEN,  // Account Analytics Read token — required for query
  // apiBaseUrl: 'https://api.cloudflare.com/client/v4', // default
});
```

**Workers durable stack:** pair this destination with
`CloudflareReliabilityAdapter` **inside the same Durable Object**. The DO owns
the journal/DLQ (durability); AE owns the dataset (queryability). Outside a DO,
AE alone is volatile. Same idea as ClickHouse/Turso + `FileReliabilityAdapter`,
moved to Workers.

> Write-only is expected. `binding` alone is enough for `bulkInsert`; `dataset` +
> `accountId` + `apiToken` alone is enough for `query`. Provide just the binding
> where you write, and add the SQL creds where you query. `query` without creds
> throws `dataset, accountId, and apiToken are required for query`.

| | |
|--|--|
| **Deps** | None |
| **Best for** | Durable Workers — DO journal + AE warehouse |
| **Runtime** | Workers (`analytics_engine_datasets` binding + SQL API) |
| **Idempotency** | None on write — WAL/DLQ replay writes duplicate points; `query` collapses by `id` |
| **Prune** | No-op (3-month platform TTL). Does not throw `prune_incomplete`. |
| **Writes** | `writeDataPoint` sync, fire-and-forget |
| **Query** | SQL API — `POST /accounts/:accountId/analytics_engine/sql`, `FORMAT JSONEachRow` |
| **Integrity** | `prevHash` / `contentHash` in blobs 8/9 |

Fixed schema (one AE data point):

| Slot | Field |
|---|---|
| `index1` | `log.tenantId ?? tenantId ?? ''` |
| `blob1`–`blob4` | `id`, `actorId`, `action`, `entityId ?? ''` |
| `blob5` | `createdAt` ISO (AE `timestamp` is write-time, not this) |
| `blob6`–`blob9` | `ipAddress`, `userAgent`, `prevHash`, `contentHash` (each `?? ''`) |
| `blob10`–`blob12` | `oldValues`, `newValues`, `metadata` JSON or `''` |
| `double1` | `Date.parse(createdAt)` epoch ms |

- ≤ 250 `writeDataPoint` calls per Worker invocation. `bulkInsert` throws **before** writing when a batch exceeds 250, so an oversized recovery wave DLQs instead of silently dropping overflow. Hosts must keep `maxRecoveryBatch` ≤ 250 (the default batcher `maxSize` is already 100).
- Index ≤ 96 bytes; total blob bytes ≤ 16 KiB per point. Oversized points throw on write.
- Ingest is asynchronous and may be sampled — AE is not a compliance-grade unique store. The DO journal is the durable admission log.
- Shared dataset only — no `adapterFactory` / `database_per_tenant`.
- No DELETE: `prune` is a no-op; the platform retains ~3 months. `query` always bounds `WHERE timestamp >= NOW() - INTERVAL '91' DAY`.

Exported helpers:

```ts
import {
  ANALYTICS_ENGINE_SCHEMA,
  toAnalyticsEngineDataPoint,
  fromAnalyticsEngineRow,
} from 'logbun/adapters/cloudflare-analytics-engine';
```

`toAnalyticsEngineDataPoint` maps a record to the fixed slot schema. `fromAnalyticsEngineRow` is the inverse used on SQL API rows. Empty string fields round-trip as omitted (`undefined`), same as ClickHouse `tenant_id`.

Source: https://logbun-docs.abshahin.workers.dev/reference/adapters/index.mdx
