Skip to main content
The shape: one Worker is the whole backend. It verifies each delivery, drops duplicates with a KV lookup, answers 200 immediately, and reads the session afterwards in ctx.waitUntil. Here the work is storing each finished turn’s reply in KV; replace handle with yours. The verifier uses only Web Crypto, so the same function runs on Workers, Deno, Bun and Node 20+ unchanged.

The Worker

crypto.subtle.verify does the comparison, which makes it constant-time by construction — there is no hex-string equality to get wrong. Signature first, then the clock. Checking the timestamp before the signature would let an unauthenticated caller learn your tolerance window. The order above rejects anything unsigned without saying why. A turn event can carry turn_id: null — when there was no turn record to name. handle reads the latest turn instead of building /turns/null, which would fail inside waitUntil where nobody sees it. The dedupe key is session_id, type and created_at. A redelivery of the same event carries the same three. See webhooks.

Configuration

Then subscribe the Worker’s URL:
KVNamespace and ExecutionContext come from @cloudflare/workers-types, which wrangler init installs.

What waitUntil does not promise

The delivery is marked seen and answered 200 before handle runs. If handle then fails, Gobare will not retry — as far as it knows, you have it. For work that must not be lost, send the event to a Cloudflare Queue instead of waitUntil, and do the work in the queue consumer, which retries on its own. The webhook route stays the same: verify, dedupe, enqueue, answer.

What was verified

The Worker’s fetch handler was run under Node 24 — whose Request, Response and crypto.subtle are the same Web APIs Workers provides — with KV replaced by an in-memory map. Every Gobare API call went to production, and every delivery was signed with Gobare’s own signing function: Not verified: the Workers runtime itself, and KV’s eventual consistency — two deliveries of one event reaching two data centres within the same second can both pass the SEEN check. handle reads current state, so running twice is harmless here; if yours is not, dedupe in a Durable Object instead.

Next