#569 Ingest Resend Delivery Events - #594
Open
b-at-neu wants to merge 4 commits into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
b-at-neu
commented
Aug 24, 2026
b-at-neu
left a comment
Collaborator
Author
There was a problem hiding this comment.
Code Review — Cycle 1 · needs revision
2 open — 1 🟡 Low, 2 ⚪ Nit (see inline)
Lets the pure delivery-events module reuse the lazy Resend client without importing the prisma-bound send module.
Adds an app/api/webhooks/resend route that verifies Resend's raw-body signature, then applies email.delivered/bounced/complained/suppressed events to the matching EmailLog row through a status-rank guard so out-of-order and duplicate events are no-ops by construction. Rewrites CLAUDE.md's API-route rule as an explicit allowlist (Better Auth's route plus this webhook) since a route can no longer be justified by a single hardcoded exception.
Unit: real signature verification (tampered body, missing header, stale timestamp), the event-to-status mapping, and the rank table. DB: each event type against a seeded row, delivered-after-bounced, replay idempotency, unknown email_id, a throwing verify, and an unhandled event type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b-at-neu
force-pushed
the
569-ingest-resend-delivery-events
branch
from
August 24, 2026 22:12
2bd9c9a to
dcbe7c9
Compare
|
Deployment failed for project aplio with the following error: Learn More: https://vercel.com/sga-operational-affairs-projects?upgradeToPro=build-rate-limit |
Collaborator
Author
Revision — Cycle 1fixed R1-N1, R1-N2, R1-L1 · dcbe7c9 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #569
Summary
app/api/webhooks/resend/route.ts, verifying Resend's raw-body signature and applyingemail.delivered/bounced/complained/suppressedevents to the matchingEmailLogrow.EMAIL_STATUS_RANKinlib/email/delivery-events.ts): an event only applies where it strictly outranks the row's current status, so out-of-order events and replays are no-ops by construction — no read-then-write, no transaction, no race.email_ids return 200 without touching the database, so Resend never retries something we can't or won't process.CLAUDE.md's API-route rule as an explicit allowlist (Better Auth's route + this webhook), framed so the next legitimate route (Manager Daily Digest #549's cron endpoint) appends a line instead of rewriting the rule.Changes
CLAUDE.md— API-route rule is now an explicit allowlist.docs/ENGINEERING.md— one line thatEmailLogdelivery state is provider-reported and eventually consistent; the two/api/auth-only mentions now point at the allowlist.lib/email/client.ts— new;getResend()moved out ofresend.tsso the pure delivery module can reuse it without importing the prisma-bound send module.lib/email/resend.ts— importsgetResendfrom./client; no behavior change.lib/email/delivery-events.ts— new; the status rank table, the zod payload schema, event→field mapping, signature verification, and the atomicupdateManywrite.app/api/webhooks/resend/route.ts— newPOSThandler: verify → classify → parse → apply → respond..env.example,README.md— documentRESEND_WEBHOOK_SECRET; drop the stale "four required variables" line the table already contradicted.tests/unit/email-delivery-events.test.ts,tests/db/email-delivery-webhook.test.ts— new.Testing plan
https://<preview>.vercel.app/api/webhooks/resend) subscribed toemail.delivered,email.bounced,email.complained,email.suppressed; put its signing secret inRESEND_WEBHOOK_SECRETfor the preview environment.EmailLogrow flipssent→deliveredwith adeliveredAtwithin a few seconds.bounced@resend.dev), then confirm the row showsbounced,bounceType: Permanent, and the bounce message inerror.curl -X POST .../api/webhooks/resend -d '{}'with no signature headers → 400, and nothing new inEmailLog.curla real payload with one body character changed → 400.curla well-formedemail.deliveredwith anemail_idthat matches no row → 200 with the ignored body; confirm no row was written.git diffthe docs:CLAUDE.md's allowlist names both routes and reads as appendable;docs/ENGINEERING.mdno longer names/api/authas the sole exception.Automated checks
npm run prettier:check— passnpm run eslint:check— passnpm run tsc:check— passnpm run test— 418 passed (21 files), run against a local throwaway Postgres container since the worktree's default port 5432 was occupied by an unrelated containerNotes
getResend()moving tolib/email/client.tsis the one change to existing code, and is a pure relocation (no behavior change).RESEND_API_KEYeven though verification is pure crypto — the SDK constructor throws without one.email_idis surfaced via the 200 response body (visible per-attempt in Resend's dashboard) rather than a log line — §4 has no permitted logging site here andconsole.erroron a routine no-op would be noise.