logbun
v1.1.0
Published
Runtime-agnostic audit logging for Node.js, Bun, Deno, and Cloudflare Workers.
Maintainers
Readme
Logbun
Runtime-agnostic audit logging for Node.js, Bun, Deno, and Cloudflare Workers.
Zero runtime dependencies on the core package. Type-safe actions. Fire-and-forget or awaitable durable enqueue. Pluggable reliability (memory, filesystem WAL/DLQ, Cloudflare Durable Object SQLite).
Package version: 1.1.0 · ES2022 / Web APIs at the root (no node:, bun:, or process in the root graph).
Documentation map
| Doc | Contents |
|-----|----------|
| www/ | Documentation site (Nimbus / Astro) — bun run docs:dev |
| This README | Install, quick start, runtimes, checklist |
| docs/README.md | Markdown docs index (same material, in-repo) |
| docs/migration-0.2.1-to-1.0.md | 0.2.1 → 1.0 migration |
| docs/architecture.md | Pipeline, reliability, pooling |
| docs/configuration.md | Config reference |
| docs/api-reference.md | Public API |
| docs/adapters.md | Destination adapters |
| docs/plugins.md | Elysia & Hono |
| docs/production.md | Multi-replica ops, integration tests |
| docs/changelog-notes.md | Capability notes (1.1.0) |
Features
fire()— never throws; optionalcontext.waitUntilfor WorkersfireAsync()— awaits full enqueue / journal (or DLQ escalation); may rejectquery()— newest-first destination pages (default limit 50, capmaxQueryLimit)flush()/runMaintenance()— host-scheduled drain + DLQ retry + retention- Reliability adapters — memory (volatile), filesystem, Cloudflare DO
- Type-safe actions — generic
AuditLogger<TActions> - Multi-tenant — shared DB or
database_per_tenant+ pool +adapterFactory - Backpressure — per-tenant queues, global caps, fair-share dumps
- Integrity chain — optional
prevHash/contentHash - Safety — redaction, payload/string caps, query limits
- Tree-shakable — adapters, plugins, durability on subpaths
Installation
npm install logbun
# or: bun add logbun / pnpm add logbun
# Deno: import from npm:logbun (grant FS permissions when using filesystem durability)Optional peers
npm install @libsql/client # Turso
npm install @clickhouse/client # ClickHouse
npm install elysia # logbun/plugins/elysia
npm install hono # logbun/plugins/hono
# BunSQLiteAdapter: bun:sqlite only (logbun/adapters/bun-sqlite)Package exports
logbun
logbun/durability/filesystem # Node/Bun/Deno (node:fs)
logbun/durability/cloudflare # Workers Durable Object SQLite (ESM-only; no CJS)
logbun/adapters/bun-sqlite
logbun/adapters/turso
logbun/adapters/clickhouse
logbun/adapters/cloudflare-analytics-engine
logbun/plugins/elysia
logbun/plugins/honoQuick start
Volatile (default — in-memory reliability)
import { AuditLogger } from 'logbun';
import { BunSQLiteAdapter } from 'logbun/adapters/bun-sqlite';
const audit = new AuditLogger({
namespace: 'my-app',
adapter: new BunSQLiteAdapter({ path: '.logbun/audit.db' }),
});
await audit.ready;
audit.fire('user.created', { actorId: 'u1', tenantId: 't1' });
await audit.fireAsync('user.updated', { actorId: 'u1', tenantId: 't1' });
// Request runtimes: await fireAsync + flush for delivery guarantees
await audit.flush();
await audit.shutdown();Durable filesystem (Node / Bun / Deno)
import { AuditLogger, ENTERPRISE_DEFAULTS, type IAdapter } from 'logbun';
import { FileReliabilityAdapter } from 'logbun/durability/filesystem';
// Supply your runtime's destination adapter (Postgres, HTTP collector, etc.).
declare const destination: IAdapter;
// Inject this from the runtime-specific entrypoint; keep it unique per replica.
declare const instanceId: string;
const reliability = new FileReliabilityAdapter({
// Reliability namespace isolates WAL/DLQ/lock on disk (per replica).
namespace: instanceId,
dataDir: '.logbun',
wal: { fsync: true },
dlq: { fsync: true },
});
const audit = new AuditLogger({
...ENTERPRISE_DEFAULTS, // mode: 'durable', requireTenantId: true
// Logger namespace is validated at bootstrap; it is not the disk path.
namespace: 'my-app',
reliability,
adapter: destination,
redactPaths: ['password', 'token'],
retention: { days: 90 },
});
await audit.ready;
await audit.fireAsync('course.created', {
tenantId: 'tenant_123',
actorId: user.id,
entityId: course.id,
});
// Host schedule (cron / supervisor):
await audit.runMaintenance();
await audit.shutdown();Read the instance ID in the runtime-specific entrypoint, then pass it to the shared setup above:
// Node.js
const instanceId = process.env.INSTANCE_ID ?? 'my-app-instance-1';// Bun
const instanceId = Bun.env.INSTANCE_ID ?? 'my-app-instance-1';// Deno (requires --allow-env=INSTANCE_ID)
const instanceId = Deno.env.get('INSTANCE_ID') ?? 'my-app-instance-1';Deno filesystem permissions:
deno run --allow-env=INSTANCE_ID --allow-read=./.logbun --allow-write=./.logbun --allow-sys=uid,gid app.ts
The path-scoped grant supports first-run creation when .logbun does not yet
exist and makes unverifiable lock owners fail closed. Add --allow-run if this
process must automatically recover a lock left by a crashed Deno process; live
exclusivity itself does not require it. See the
filesystem threat model for
the capability-boundary, stale-lock cleanup, and same-user limitations.
For Bun's built-in SQLite destination specifically, import
BunSQLiteAdapter from logbun/adapters/bun-sqlite in a Bun-only module.
Cloudflare Durable Objects
import { AuditLogger } from 'logbun';
import { CloudflareReliabilityAdapter } from 'logbun/durability/cloudflare';
import {
CloudflareAnalyticsEngineAdapter,
type AnalyticsEngineDatasetLike,
} from 'logbun/adapters/cloudflare-analytics-engine';
interface Env {
AUDIT_AE: AnalyticsEngineDatasetLike; // wrangler analytics_engine_datasets binding
ACCOUNT_ID?: string; // optional — enables AuditLogger.query()
AE_API_TOKEN?: string; // optional — secret with Account Analytics Read
}
export class AuditDO {
private audit: AuditLogger;
constructor(private ctx: DurableObjectState, private env: Env) {
this.audit = new AuditLogger({
namespace: 'do',
mode: 'durable',
reliability: new CloudflareReliabilityAdapter({ state: ctx }),
adapter: new CloudflareAnalyticsEngineAdapter({
binding: env.AUDIT_AE, // write (writeDataPoint)
dataset: 'logbun_audit', // query
accountId: env.ACCOUNT_ID, // optional — query
apiToken: env.AE_API_TOKEN, // optional — query
}),
});
}
async alarm() {
await this.audit.runMaintenance();
}
}Standard Workers should call a DO binding. Use fireAsync + journal for
admission that survives request end; do not treat detached volatile fire()
as durable in isolate-scoped runtimes.
If the DO journal commits but getAlarm / setAlarm fails, fireAsync
rejects with DurableAdmissionSchedulingError and
durableAdmissionCommitted === true. Do not resubmit that audit event; call
requestMaintenance() after the scheduler recovers. Use the exported
isDurableAdmissionSchedulingError(error) guard across package entrypoints.
Hono / Elysia waitUntil
When executionCtx.waitUntil exists (Workers), the Hono plugin registers
fire() admission tasks automatically. Elysia does not inject ExecutionContext
— pass getWaitUntil. Client IP uses append-style XFF (nginx/ALB): with
trustedProxyCount: 1, '1.1.1.1, 203.0.113.50' yields 203.0.113.50 (the
hop the proxy added). A short list (length < N) is not trusted. Default
trustedProxyCount is 0. getTenantId must come from an authenticated
session or JWT, never a raw x-tenant-id header unless the gateway overwrites
it. The Elysia plugin passes the full derive context into getTenantId.
import { createAuditMiddleware } from 'logbun/plugins/hono';
// Hono: app.use('*', createAuditMiddleware(audit, { trustedProxyCount: 1 }));
import { auditPlugin } from 'logbun/plugins/elysia';
// Elysia: app.use(auditPlugin(audit, { trustedProxyCount: 1 }));Capability matrix
| | Volatile (root default) | File reliability | CF DO reliability | |--|-------------------------|------------------|-------------------| | Runtimes | Node, Bun, Deno, Workers | Node, Bun, Deno | Workers DO | | Journal | no (optional memory) | WAL segments | DO SQLite | | DLQ | memory | files + opaque IDs | DO SQLite | | Survive process death | no | yes | yes | | Host maintenance | yes (DLQ/retry/retention) | yes | DO alarm |
Production checklist
await audit.readybeforefire/fireAsyncin durable mode (the pre-ready buffer is volatile even whenmode: 'durable')mode: 'durable'+ persistent reliability with uniquenamespaceper replica- Prefer
fireAsyncwhen callers must know admission succeeded; handle its committed-but-unscheduled error as described above - Schedule
runMaintenance()(or DOalarm) and listen for its throws (flush, DLQ scan, or prune failure, includingprune_incompleteon SQLite/Turso). One call is a bounded pass — schedule a follow-up if DLQ/dlqDead/recovery work remains. Alert ondlqDead. - On request-scoped volatile hosts:
await fireAsync(...); await flush() - Set
requireTenantId: true(or useENTERPRISE_DEFAULTS) for multi-tenant SaaS - Observe via
onEvent,getStats(),getStatsDetailed()
Migrating from 0.2.1
See docs/migration-0.2.1-to-1.0.md for
FileReliabilityAdapter, DLQ IDs, host maintenance, Bun SQLite path rename,
Deno permissions, and Cloudflare DO / waitUntil details.
License
MIT
