#2014 feat(nurture): an outbound transport that reports its outcome, and per-channel send status
Opened by keeb · 9/4/2026
Parent: the product-nurture epic. Blocks every send.
Problem
The ask includes send status. It cannot be recorded through the transport we have:
// lib/infrastructure/resend-mailer.ts
if (!res.ok) {
logger.error("Resend API error", { ... }); // and then returns void
}A Resend 4xx — a suppressed address, an invalid recipient, a rate limit — is
logged and reported to the caller as success. A ledger fed by that transport
would show every nurture send as sent, including the ones that never
arrived. The same swallow means a retryable failure is indistinguishable from a
permanent one, so a naive retry loop would either never retry or retry forever.
What ships
An outcome-returning send. A result type distinguishing:
delivered— Resend accepted it, with the provider message id recordedrejected— a 4xx we must not retry (bad address, suppressed); terminaldeferred— a 429 or 5xx; retryable with backoffskipped— noRESEND_API_KEY, which is the local-dev and CI default and must stay a first-class outcome rather than a fake success
Introduced alongside the existing Mailer rather than by changing it —
lib/auth.ts and the trial mails have their own posture, and widening this
change into them is how a small fix becomes a risky one.
Per-channel status on the nurture_send row, with attempt count, last
error, and next-attempt instant. A deferred row is retried by the projector
with backoff; a rejected row is terminal and rendered as such in the panel.
The claim happens before the send, and it stays claimed on failure. This is
at-most-once, deliberately, and it is the same call sendWelcomeEmail
documents: a duplicate nurture email is worse than a missing one. A deferred
retry is the single exception, bounded by attempt count.
Verification
tests/infrastructure/nurture_mailer_test.ts— a stub Resend answering 200, 422, 429, 500 and a network throw; assert each maps to the right outcome and that a 4xx is never reported as delivered.tests/app/nurture_delivery_test.ts— a deferred send retries with backoff and eventually gives up; a rejected send never retries; a claimed row is never re-claimed.- Drive one real send to a real address on the local stack with a live key, and confirm the recorded provider message id matches the Resend dashboard.
Out of scope
Which flows send, and what they say.
In Progress
Click a lifecycle step above to view its details.