Skip to content

#569 Ingest Resend Delivery Events - #594

Open
b-at-neu wants to merge 4 commits into
devfrom
569-ingest-resend-delivery-events
Open

#569 Ingest Resend Delivery Events#594
b-at-neu wants to merge 4 commits into
devfrom
569-ingest-resend-delivery-events

Conversation

@b-at-neu

Copy link
Copy Markdown
Collaborator

Closes #569

SESSION REQUIRED: touches CLAUDE.md / .claude/** — a dispatched agent can't edit those

Summary

  • Adds app/api/webhooks/resend/route.ts, verifying Resend's raw-body signature and applying email.delivered/bounced/complained/suppressed events to the matching EmailLog row.
  • Correctness comes from a status-rank table (EMAIL_STATUS_RANK in lib/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.
  • Unhandled event types and unmatched email_ids return 200 without touching the database, so Resend never retries something we can't or won't process.
  • Rewrites 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 that EmailLog delivery 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 of resend.ts so the pure delivery module can reuse it without importing the prisma-bound send module.
  • lib/email/resend.ts — imports getResend from ./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 atomic updateMany write.
  • app/api/webhooks/resend/route.ts — new POST handler: verify → classify → parse → apply → respond.
  • .env.example, README.md — document RESEND_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

  • In the Resend dashboard, add a webhook pointing at the preview URL (https://<preview>.vercel.app/api/webhooks/resend) subscribed to email.delivered, email.bounced, email.complained, email.suppressed; put its signing secret in RESEND_WEBHOOK_SECRET for the preview environment.
  • Sign in to trigger a real OTP send, then confirm the new EmailLog row flips sentdelivered with a deliveredAt within a few seconds.
  • Trigger a hard bounce (e.g. send to bounced@resend.dev), then confirm the row shows bounced, bounceType: Permanent, and the bounce message in error.
  • Replay that same bounce from Resend's dashboard ("Resend event" on the webhook attempt) — the row is unchanged and the response body says it was ignored.
  • curl -X POST .../api/webhooks/resend -d '{}' with no signature headers → 400, and nothing new in EmailLog.
  • curl a real payload with one body character changed → 400.
  • curl a well-formed email.delivered with an email_id that matches no row → 200 with the ignored body; confirm no row was written.
  • Confirm the Resend dashboard shows no failed attempts for the endpoint (a 500 would mean it's retrying).
  • git diff the docs: CLAUDE.md's allowlist names both routes and reads as appendable; docs/ENGINEERING.md no longer names /api/auth as the sole exception.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm 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 container

Notes

  • No migration — every column and enum member shipped in Email Foundation And Send Log #547.
  • getResend() moving to lib/email/client.ts is the one change to existing code, and is a pure relocation (no behavior change).
  • The webhook still needs RESEND_API_KEY even though verification is pure crypto — the SDK constructor throws without one.
  • Only one environment can receive events per configured Resend endpoint, so preview testing means temporarily repointing the webhook or replaying signed payloads by hand.
  • An unmatched email_id is 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 and console.error on a routine no-op would be noise.

@b-at-neu b-at-neu added the claude Will be worked on by Claude label Aug 24, 2026
@b-at-neu b-at-neu self-assigned this Aug 24, 2026
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
aplio Ready Ready Preview Aug 24, 2026 9:51pm

@b-at-neu b-at-neu added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 24, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 1 · needs revision

2 open — 1 🟡 Low, 2 ⚪ Nit (see inline)

Comment thread lib/email/delivery-events.ts Outdated
Comment thread lib/email/delivery-events.ts Outdated
Comment thread tests/db/email-delivery-webhook.test.ts
@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Aug 24, 2026
b-at-neu and others added 4 commits August 24, 2026 18:10
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
b-at-neu force-pushed the 569-ingest-resend-delivery-events branch from 2bd9c9a to dcbe7c9 Compare August 24, 2026 22:12
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

Deployment failed for project aplio with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/sga-operational-affairs-projects?upgradeToPro=build-rate-limit

@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-N1, R1-N2, R1-L1 · dcbe7c9

@b-at-neu b-at-neu added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude Will be worked on by Claude reviewing Review agent working (in-flight)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ingest Resend Delivery Events

1 participant