You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split out of #311, which landed the Twilio half in #321.
What's missing
#321 records delivery verdicts for SMS and WhatsApp via Twilio's StatusCallback. Email has no equivalent wired up, so a hard bounce or a spam complaint is still invisible: the row reads status='sent' forever.
This matters as much as the Twilio half. Resend accepts at SMTP, bounces later, and then adds the address to its account-level suppression list — after which every subsequent send to that address is silently dropped. An owner or heir can go permanently unreachable while the dashboard reports every message as sent.
Two unknowns that need checking against the live account rather than guessing:
Correlation. We send over SMTP, so we have no Resend message id at handoff. fix(server): record what the provider says happened, not just handoff #321 now stores the SMTP 250 reply in provider_message_id on the theory that Resend puts a queue id there, but that is unverified. Needs one real send inspected to confirm the format, and whether it matches data.email_id in the webhook payload.
If it doesn't match, the fallback is matching on recipient — which is awkward because recipient_ciphertext is AEAD-sealed with a random nonce, so it isn't searchable. That would need a deterministic lookup column (e.g. an HMAC of the address under the master key), which is a design decision, not a patch.
Secret. Resend signs webhooks with Svix (svix-id, svix-timestamp, svix-signature, HMAC-SHA256 over a base64 secret). That's a new RESEND_WEBHOOK_SECRET env var and a Resend dashboard endpoint configuration — unlike the Twilio path, this one is not zero-config.
Scope
POST /webhooks/resend with Svix signature verification, including the timestamp tolerance check (replay protection)
handle email.bounced and email.complained; optionally email.delivered
reuse delivery::record_delivery, which is already provider-agnostic
document the secret and the dashboard step in DEPLOY.md
Blocks
#311 shouldn't close until this lands — half the channels reporting delivery is not the same as knowing whether messages arrive.
Split out of #311, which landed the Twilio half in #321.
What's missing
#321 records delivery verdicts for SMS and WhatsApp via Twilio's
StatusCallback. Email has no equivalent wired up, so a hard bounce or a spam complaint is still invisible: the row readsstatus='sent'forever.This matters as much as the Twilio half. Resend accepts at SMTP, bounces later, and then adds the address to its account-level suppression list — after which every subsequent send to that address is silently dropped. An owner or heir can go permanently unreachable while the dashboard reports every message as sent.
Why it wasn't done in #321
Two unknowns that need checking against the live account rather than guessing:
Correlation. We send over SMTP, so we have no Resend message id at handoff. fix(server): record what the provider says happened, not just handoff #321 now stores the SMTP
250reply inprovider_message_idon the theory that Resend puts a queue id there, but that is unverified. Needs one real send inspected to confirm the format, and whether it matchesdata.email_idin the webhook payload.If it doesn't match, the fallback is matching on recipient — which is awkward because
recipient_ciphertextis AEAD-sealed with a random nonce, so it isn't searchable. That would need a deterministic lookup column (e.g. an HMAC of the address under the master key), which is a design decision, not a patch.Secret. Resend signs webhooks with Svix (
svix-id,svix-timestamp,svix-signature, HMAC-SHA256 over a base64 secret). That's a newRESEND_WEBHOOK_SECRETenv var and a Resend dashboard endpoint configuration — unlike the Twilio path, this one is not zero-config.Scope
POST /webhooks/resendwith Svix signature verification, including the timestamp tolerance check (replay protection)email.bouncedandemail.complained; optionallyemail.delivereddelivery::record_delivery, which is already provider-agnosticBlocks
#311 shouldn't close until this lands — half the channels reporting delivery is not the same as knowing whether messages arrive.