Live demo · every value on this page came from the real API

Ledgerline, shown working

An append-only event-stream API on Cloudflare Workers. This page replays an actual recorded run (real requests, real responses, real SHA-256 digests) and explains what happens inside at each step.

Exactly-once appends Strong per-stream ordering Tamper-evident hash chain
The system in one picture

Where a write goes

The Worker at the edge is stateless; it authenticates and routes. The guarantees live in Durable Objects: tiny single-threaded actors, one per stream and one per API key. D1 (SQLite) is a read-only mirror for fast queries.

Client
Bearer key + Idempotency-Key
HTTPS
Worker, Hono router
auth · rate-limit · route · mirror
RPC, strongly consistent
RateLimiterDO
1 per API key · token bucket
StreamDO ★ authority
seq · idempotency · hash chain
mirror after confirm (INSERT OR IGNORE)
D1 read model
paginated reads · eventually consistent
  1. Authenticate. The bearer key is SHA-256-hashed and looked up in D1. No plaintext keys anywhere.
  2. Spend a token. The key's own RateLimiterDO must grant one token or the request ends 429.
  3. Append. The stream's StreamDO assigns the next seq, links the hash chain, commits atomically.
  4. Mirror. Only after the authority confirms does the Worker copy the event into D1 for reads.
Interactive · real data, real SHA-256

The chain, live. Try to tamper with it.

These are the three events from the recorded run, hashes byte-for-byte as the API returned them. Your browser is recomputing the chain right now with the same public rule the server uses. Edit any amount and watch verify catch it, then hit reset.

hash_0 = SHA-256("ledgerline:v1:" + streamId) genesis hash_n = SHA-256(hash_n-1 + "|" + canonicalJSON(payload) + "|" + seq)
GET /verify → { "valid": true }
recomputing chain in your browser…
genesis: SHA-256("ledgerline:v1:7221f193-c32f-4bed-b13e-d83a20fdf66c") =

In production, "editing a payload" means tampering with the Durable Object's storage directly; the test suite does exactly that and asserts verify reports the first broken seq. Every hash folds in the previous one, so one edit invalidates the entire suffix of the log. Deleting the newest events (truncation) is caught too: verify compares the recomputed head against the stream's meta.

Recorded run · exactly-once

Retry it all you want. One event.

The client sent the same Idempotency-Key three times: once normally, once as a network-style retry, once as a retry with a different body (a buggy or malicious client). The ledger recorded exactly one event, and every retry got the original answer back.

# first append $ curl -X POST …/streams/7221f193/events -H 'Idempotency-Key: invoice-001' \ -d '{"type":"invoice.paid","invoice":"INV-001","amount":100,"currency":"USD"}' 201 → {"seq":1,"hash":"1a902c0da42aec20…"} # same key, retried 200 → {"seq":1,"hash":"1a902c0da42aec20…"} Idempotent-Replay: true # same key, DIFFERENT body (amount: 99999) 200 → {"seq":1,"hash":"1a902c0da42aec20…"} Idempotent-Replay: true # the read model still holds the ORIGINAL payload, and it hashes to the stored hash GET …/events → payload: {"amount":100,"currency":"USD","invoice":"INV-001",…}

The key is authoritative: it's bound to the first event forever. That last line matters: an adversarial review of this codebase found (and we fixed) a subtle bug where a changed retry body could poison the read-model mirror. Now the Durable Object hands the mirror its canonical payload, so the stored payload always hashes to the stored hash.

Recorded run · rate limiting

A token bucket that shows its math

This key was minted with rate_per_min: 3. Three requests spend the bucket; the fourth is refused, and the limiter tells the client exactly when to come back: 60 s ÷ 3 tokens = 20 s to the next token.

requeststatusRateLimit-RemainingRetry-After
POST /v1/streams #1201 Created2·
POST /v1/streams #2201 Created1·
POST /v1/streams #3201 Created0·
POST /v1/streams #4429 Too Many020 s

Each API key gets its own single-threaded RateLimiterDO, so two simultaneous requests can never double-spend the same token: the same actor trick that gives streams their ordering.

Recorded run · the error contract

Every failure is a clean, typed answer

One envelope everywhere: {"error":{"code","message"}}. And note the fourth row: asking about someone else's stream returns 404, not 403, so the API never confirms that a stream exists.

scenariostatuscode
No Authorization header401unauthorized
Invalid API key401unauthorized
Append without Idempotency-Key400idempotency_key_required
Another key's stream (no existence leak)404stream_not_found
Body over 256 KiB413payload_too_large
Bucket empty429rate_limited
Wrong admin secret on POST /v1/keys403forbidden
How this was verified

Not just claimed: tested

69tests across 14 files, run inside the real Workers runtime (workerd via Miniflare): actual Durable Objects, actual D1, no mocks. Built test-first, including truly concurrent appends and DO-eviction durability.
41adversarial review agents across two audits; every confirmed bug fixed with a regression test, including a subtle hash-collision via own __proto__ JSON keys, tail-truncation blindness in verify, and a fail-open admin guard.
0type errors under TypeScript strict; clean conventional-commit history; typed 4xx limits on every input (no 500s from oversized or pathological payloads).