Skip to content

Adapters

Built-in destination adapters — Bun SQLite, Turso, ClickHouse, and Analytics Engine.

Updated View as Markdown

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

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

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

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:

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 for how they pair. Use AE for the warehouse, use the DO adapter for durability.

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 ?? ''
blob1blob4 id, actorId, action, entityId ?? ''
blob5 createdAt ISO (AE timestamp is write-time, not this)
blob6blob9 ipAddress, userAgent, prevHash, contentHash (each ?? '')
blob10blob12 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:

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close