---
title: "Custom adapter"
description: "Implement IAdapter for a destination that is not SQLite, Turso, or ClickHouse."
---

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

# Custom adapter

```ts
import type {
  IAdapter,
  LogbunLog,
  LogbunQueryFilters,
  LogbunQueryResult,
} from 'logbun';

class PostgresAuditAdapter implements IAdapter {
  async init(): Promise<void> {
// Must be idempotent: ConnectionPool always calls init() after adapterFactory.
  }

  async bulkInsert(
tenantId: string | null,
logs: LogbunLog[],
  ): Promise<boolean> {
// Prefer idempotent insert on log.id.
// return true on success; false is a soft/retryable failure.
// Prefer throwing on unexpected failures so the engine can emit error text.
return true;
  }

  async query(
tenantId: string | null,
filters: LogbunQueryFilters,
pagination: { cursor?: string; limit: number },
  ): Promise<LogbunQueryResult> {
return { logs: [], nextCursor: null };
  }

  async prune(days: number): Promise<void> {
// delete older than `days` (finite integer >= 1)
  }

  async close(): Promise<void> {}
}
```

## Contract

1. **Idempotent on `id`** so WAL replay / retries do not duplicate rows.
2. **Honor `tenantId`**: `null` = unscoped; `''` = tenant-less rows only; any other string = that tenant.
3. **Cursor**: newest-first pages (`ORDER BY id DESC`) and `nextCursor` = last row’s `id` when more pages exist. `pagination.limit` is required on `IAdapter.query`.
4. **Empty `bulkInsert`**: return `true`.
5. **Closed adapter**: `query` / `bulkInsert` / `prune` must throw when closed or not initialized (do not return empty results as if no rows existed).
6. **Integrity fields**: persist `prevHash` / `contentHash` when `integrityChain` is enabled. After restart the tip is restored from WAL, then the newest destination row (`limit: 1`).
7. **Prune**: `days` is a finite number `>= 1`. Prefer bounded batches. Built-in SQLite/Turso throw `prune_incomplete` when the 10_000 × 1_000 safety cap is hit so `runMaintenance()` can reject and the host can schedule a follow-up.

`AuditLogger.query()` throws `AuditLogger is not initialized — query unavailable` when the logger is shut down or bootstrap failed. Built-in destination adapters throw from `query` / `bulkInsert` / `prune` if they are closed or not initialized.

Source: https://logbun-docs.abshahin.workers.dev/guides/custom-adapter/index.mdx
