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).
| 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) |
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
npm install logbun
# or: bun add logbun / pnpm add logbun
# Deno: import from npm:logbun (grant FS permissions when using filesystem durability)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)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/hono
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();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.
Workers need two pieces — keep them separate:
- Reliability —
CloudflareReliabilityAdapter(logbun/durability/cloudflare, ESM-only) lives inside a Durable Object and owns the SQLite journal/DLQ. No D1/R2/KV, nonode:fs. - Destination —
CloudflareAnalyticsEngineAdapter(logbun/adapters/cloudflare-analytics-engine) owns the Analytics Engine dataset viawriteDataPoint/ SQL API. It is not durable on its own — without the DO journal,fire()is volatile.
For durable Workers, pair them inside the same DO. The DO is the admission log, AE is the warehouse (sampled, ~3 months, fire-and-forget). Workers call the DO binding.
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 — must match wrangler.jsonc dataset
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.
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 }));| 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 |
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()
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.
MIT