Skip to content

feat(server): Idempotency-Key — response-capture ledger, replay, capability flag (CWD-5) - #325

Merged
eliotlim merged 25 commits into
mainfrom
feat/cwd5-idempotency
Aug 15, 2026
Merged

feat(server): Idempotency-Key — response-capture ledger, replay, capability flag (CWD-5)#325
eliotlim merged 25 commits into
mainfrom
feat/cwd5-idempotency

Conversation

@eliotlim

Copy link
Copy Markdown
Collaborator

Problem

Idempotency covered exactly one of ~25 write routes (keyless page-create), so a client retrying any other write — and especially the coming CWD-7 offline outbox replaying after a crash — risks double-applying mutations. Board: CWD-5 (epic OB-76); implements §4 of the merged write contract (#321).

Solution

Idempotency-Key accepted on the 17 wave-1 routes (400 elsewhere): SHA-256 request fingerprint (length-delimited method/path/media-type/exact body bytes), actor-scoped, with a response-capture ledger written in the same transaction as the mutation — replay returns the captured response byte-exact and suppresses broadcasts/edit-log; fingerprint mismatch → 409 idempotency-key-reused; destroy replays resolve before existence checks; CAS 409s consume no key; 7-day GC. GET /api/instance now advertises writeContract: 1 so outboxes never auto-replay against old servers. Composed with CWD-4's persister fence (claim/replay outside, fence inside — replay never re-enters the fence). Restoring correct nested-savepoint semantics on Postgres (native driver scopes) came out of review and hardens the whole store.
Key files: packages/server/src/{app,store,db,dbCore,migrations}.ts. New deps: none.

Before / After

No visual change — server write semantics.

Behavior Before After
Retried write (any wave-1 route) Double-applied Replay returns captured response, one durable mutation (concurrent-duplicate test: one row, same response both)
Crash between commit and response Client can't know; outbox re-applies Replay returns the original response incl. destroys (204 replays after target is gone)
Same key, different request Undefined 409 idempotency-key-reused, outbox marks INDETERMINATE
Old server + new outbox Silent double-apply risk writeContract capability gate
Unauth'd request with header n/a No buffering — capture sits behind every auth/scope gate + key-format check

Test procedure

pnpm --filter @book.dev/server test -- src/idempotencyRoutes.test.ts src/idempotencyLedger.test.ts
pnpm verify

Operational notes

Migration 0028 (drop/recreate of the unreleased 0027 ledger as TEXT — the JSONB variant existed on-branch for 44 minutes and is preserved for immutability). GC via the existing purge scheduler; TTL <=0 = forever preserved. Bare-204 destroy bodies stay as-is (CWD-2's remit, recorded on its board acceptance).


Verify: green (full pnpm verify on the merged tip; 86/86 at the fence×idempotency intersection). Reviews cleared: code (Quinn — full round C1-C8 incl. two blockers fixed, then targeted C5/C2 verification: "downstream CWD-2/6/7 can build on this").

🤖 Generated with Claude Code

https://claude.ai/code/session_018RUicirLRuuZvjJN13N6yN

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app.book.pub Ready Ready Preview Aug 15, 2026 3:11am

Request Review

@eliotlim
eliotlim merged commit a55adbc into main Aug 15, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant