Built for Codex, Claude Code, and other repo-aware agents. It reviews what you already have instead of prescribing a new webhook framework.
Free webhook checkup + sample
See what your Rails webhook code is assuming.
Give a repo-aware coding agent one focused job: trace your webhook boundary and tell you where signatures, retries, duplicates, ordering, or failure handling are relying on luck.
Read the opening of Make It Safe to Retry, where a happy-path webhook starts becoming production code.
Webhook architecture checkup
Give your agent a narrower question.
Open the Rails app in Codex, Claude Code, Cursor, or another repo-aware agent. Paste the prompt below and keep the first pass read-only.
01 / Run the checkup
Webhook architecture checkup
Review the inbound webhook implementation in this existing Ruby on Rails application.
This is a diagnosis-only pass. Do not modify files, install dependencies, run migrations, or contact production services. Inspect enough of the repository, schema, routes, jobs, tests, configuration, and provider integration code to trace the real behavior before making findings.
Do not replace a coherent local architecture just because you prefer another pattern. The goal is to find correctness gaps, contradictory precedent, and important boundaries that are currently left to luck or assumption.
TRACE FIRST
For each inbound webhook provider, trace one real delivery through:
provider -> route -> authentication/signature verification -> durable receipt -> queue handoff -> handler -> local business changes -> processed/failed state
If the application creates or removes provider-side webhook registrations, trace that lifecycle too.
CHECK SIX BOUNDARIES
1. Authenticity and request parsing
- Is the provider signature verified using the exact bytes and credential its current official documentation requires?
- Does verification happen before parsed payload data is trusted or persisted?
- Do missing, malformed, expired, or incorrect signatures fail closed without persistence or enqueueing?
- If provider-specific behavior is uncertain, verify current official provider documentation when possible. Otherwise mark it Unknown instead of guessing from model memory.
2. Durable handoff, duplicates, and lifecycle
- What must succeed before the controller returns 2xx?
- Is work durably recoverable before acknowledgement?
- What provider value identifies one delivery or event, and is that identity protected by a database UNIQUE constraint rather than only an exists? check or model validation?
- Does the system distinguish received from processed?
- Can failed work remain retryable instead of being skipped because a receipt row already exists?
- Can duplicate queued workers safely converge on one final result?
3. Retry and ordering
- Separate provider delivery retry from Active Job / queue retry. What owns each side of the successful HTTP response?
- Are transient job failures actually retried, while permanent failures remain visible?
- Does the code assume webhook delivery order the provider does not guarantee?
- If stale changes must be rejected, is there a real provider timestamp/version or serialization strategy rather than Rails receipt time pretending to be provider order?
4. Transactions and external effects
- Are locks and transactions limited to local critical work?
- Does any provider API call, email, HTTP request, or other slow external side effect happen while a database lock is held?
- If a handler can repeat an irreversible external effect, is there a provider idempotency key, stable business key, outbox, or equivalent protection?
5. Provider registration, payloads, and secrets
- If remote hooks are registered programmatically, can retries reconcile remote state instead of blindly creating duplicates after a partial failure?
- Does reconciliation inspect every page/result it needs and restore the complete intended remote configuration?
- Are raw bodies and parsed payloads retained intentionally?
- Could webhook bodies, signatures, signing credentials, or authorization values leak into request, SQL, or structured logs?
6. Tests
Look for focused tests that prove the boundaries that actually matter here: exact raw-body verification, invalid signatures, signed malformed JSON, duplicate delivery, database uniqueness, handler failure followed by retry, duplicate workers, ordering/current-state behavior where relevant, and provider-registration recovery where present.
REPORT ONLY MATERIAL FINDINGS
Return:
1. FLOW MAP
A compact table: Provider | Route | Verification | Durable receipt | Queue | Handler | Final state.
2. WHAT IS ALREADY SOLID
The strongest webhook precedents in the repository, with file paths. These are the patterns a coding agent should preserve.
3. FINDINGS
For each material issue include severity, confidence, concrete file/method evidence, the real failure mode, and the smallest reasonable fix. Prefer roughly 5-10 meaningful findings over a generic checklist dump.
4. MISSING TESTS
Only tests that would prove an important boundary currently left to assumption.
5. AGENT HANDOFF
Finish with a short section titled "What a coding agent should know before changing these webhooks" summarizing the local architecture, invariants worth preserving, and places where a future agent should not invent behavior.
Do not recommend a rewrite because the app differs from your preferred stack. Trace call sites and nearby tests before judging. Rank signature correctness, recoverability, duplicate safety, data integrity, idempotency, and secret handling above style.02 / Read a sample
From “Make It Safe to Retry”
The happy path in a webhook demo is usually about ten lines long. Production behavior lives in everything that happens when those lines run twice, stop halfway through, or run in two workers at the same time.
We already have the pieces of a safer flow, so let's look at why each one exists and turn it into a pattern you can reuse.
Received is not processed
A common first Stripe implementation creates its event record before the event-specific work runs. That is tempting because the row also acts as the duplicate check:
return if StripeEvent.exists?(stripe_event_id: event_id)
StripeEvent.create!(stripe_event_id: event_id)
update_the_account!There is a nasty failure hiding in the order. If update_the_account! raises, Active Job retries the job, sees the event record, and returns without doing the work that failed. The duplicate guard turned a recoverable error into a permanently skipped event.
A delivery record and a processed marker answer different questions:
receivedmeans an authentic request reached the ingress endpoint and was durably recorded.processingmeans a worker has started an attempt.processedmeans the handler's local changes succeeded.failedmeans an attempt raised and should be visible to an operator and retryable by the queue.
Only processed is a reason to skip the handler forever.
Let the database arbitrate duplicates
The index we added is the real delivery-level idempotency boundary:
add_index :webhook_deliveries,
[:provider, :external_id],
unique: truecreate_or_find_by! tries the insert and relies on that index if another request wins. This avoids an exists? followed by create!, but it doesn't mean only one background job can be enqueued. Two ingress requests can both receive the same row and both enqueue it.
That is why ProcessStripeWebhookJob also uses delivery.with_lock. The second worker waits for the first transaction. After it gets the lock, it reloads the row and returns if the first worker marked it processed.
The local account update and the final delivery update happen inside the same database transaction created by with_lock:
delivery.with_lock do
return if delivery.processed?
delivery.update!(status: "processing")
update_local_records!
delivery.update!(status: "processed", processed_at: Time.current)
endIf update_local_records! raises, Active Record rolls the transaction back. There is no half-finished account update with a processed delivery beside it.
Holding a row lock while changing a few local records is understandable and useful for this pattern. Don't make a slow remote API call while that lock is held. In the Stripe job, event retrieval happens before with_lock for that reason.
The chapter keeps going: failure and retry state, provider retry vs. queue retry, operator reprocessing, delivery IDs vs. business IDs, ordering, exactly-once external effects, and payload retention.
Get the full 44-page guide + Agent Companion · $12