Build on the supported package contract
Use @absolutejs/audit-s3 through its supported public entry points.
@absolutejs/audit-s3v0.1.1betaObservabilityS3-compatible AuditSink for @absolutejs/audit. Buffered JSONL writes to AWS S3 / Cloudflare R2 / Backblaze B2 / MinIO. Time-sortable object keys; WORM-bucket-friendly for compliance retention.
bun add @absolutejs/audit-s3S3-compatible AuditSink for @absolutejs/audit.
Buffered JSONL writes to AWS S3 / Cloudflare R2 / Backblaze B2 / MinIO — any store with a "put a string at a key" API.
WORM (write-once-read-many) buckets give legal hold for compliance
retention (SOC2, HIPAA, FedRAMP). The hash-chain in @absolutejs/audit's withIntegrity() gives tamper-evidence; WORM prevents deletion even by an admin.
Lifecycle policies handle retention windows without a cron job.
"Move to Glacier after 90 days, delete after 7 years" is one bucket policy.
Cheap. Cold-tier storage costs cents/GB-month.
Queryable later via Athena, DuckDB, or s3 ls | xargs cat.
S3 objects are immutable, so the sink buffers events and flushes as JSONL files keyed by time. Object keys are lexically sortable; s3 ls audit/ returns events in chronological order.
AWS SDK v3
Cloudflare R2 (Workers)
MinIO
Same as AWS SDK — MinIO speaks S3 protocol. Point the S3Client at your MinIO endpoint and the adapter doesn't care.
Default keyFor produces:
Date prefix (2026-05-30/) — lifecycle policies key off this.
Time component (19-42-15.123-) — UTC HH-MM-SS.mmm. Lexical sort
= chronological order.
8 hex chars random tail — collision-resistant for two flushes at
the same millisecond.
.jsonl — one JSON-encoded event per line, trailing newline.
Override via the keyFor option for tenant-fan-out or hourly partitions.
Whichever fires first:
Trigger — Default — Option
Buffer reaches event count — 1000 — maxBatchSize
Buffer reaches byte count — 5_000_000 (5 MB) — maxBatchBytes
Time since last flush — 5_000 ms — flushIntervalMs
Manual — (caller) — await sink.flush()
Close — (caller) — await sink.close()
Set flushIntervalMs: 0 to disable the periodic timer (size-only flushing).
Unflushed events are lost on process kill. For stricter durability, pair the S3 sink with a synchronous sink (Postgres) for critical events — S3 is the long-term archive, not the source of truth between flushes. Lower flushIntervalMs to shrink the loss window at the cost of more S3 PUTs.
list / prune — not implemented. Read audit logs out of S3 via
Athena / s3 ls / DuckDB; enforce retention via S3 lifecycle policies. The sink is write-only.
Retry on PUT failure — onPutError callback fires once; the batch
is dropped. Wire your own retry queue if you need at-least-once.
Multipart upload — every batch is one PUT. If your batches grow
past S3's 5GB PutObject limit you have other problems.
The tamper-evident chain from withIntegrity() works across batch boundaries automatically. Each event is hashed at append time against the prior event's hash; the S3 sink only buffers + flushes — it doesn't touch the chain. To verify a chain that spans multiple S3 objects:
Outcomes
Use @absolutejs/audit-s3 through its supported public entry points.
Hardening checklist
Follow in order
Working example for AWS SDK v3.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createAudit, withIntegrity, memorySink } from '@absolutejs/audit';
import { createS3AuditSink } from '@absolutejs/audit-s3';
const s3 = new S3Client({ region: 'us-east-1' });
const audit = createAudit({
sinks: [
memorySink({ max: 1000 }), // hot tail for queries
withIntegrity( // tamper-evident
createS3AuditSink({
put: async (key, body, contentType) => {
await s3.send(new PutObjectCommand({
Bucket: 'my-audit-bucket',
Key: key,
Body: body,
ContentType: contentType,
}));
},
prefix: 'audit/prod/',
flushIntervalMs: 5_000,
}),
{ secret: process.env.AUDIT_SECRET, writerId: 'shard-A' }
),
],
});
await audit.append({
kind: 'auth.login',
actor: 'user-123',
metadata: { ip: '10.0.0.1' },
});
// On graceful shutdown:
await audit.close();Working example for Cloudflare R2 (Workers).
import { createS3AuditSink } from '@absolutejs/audit-s3';
const sink = createS3AuditSink({
put: async (key, body, contentType) => {
await env.AUDIT_BUCKET.put(key, body, { httpMetadata: { contentType } });
},
});Supported entry points declared by this package manifest.
Package entry point declared in package.json.
Scripts declared by this package manifest.
Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
The single S3 operation the sink needs. Implement against your preferred SDK; the sink calls put(key, body, contentType) with 'application/x-ndjson' and expects a resolved Promise on success or a rejection on failure.
type S3PutFn = (key: string, body: string, contentType: string) => Promise<void>;@absolutejs/audit-s3