AUTH4D-10 — Transactional email delivery
This ticket defines the internal verification-email delivery contract. The public verification routes remain owned by API feature composition.
Delivery flow
- The API creates a tenant-scoped outbox record before publishing a Queue message.
- Recipient address, verification code, product label, and message expiry are serialized and
encrypted with the
email:verification:deliverypurpose and the tenant ID as authenticated context. The outbox stores this ciphertext; queue messages carry the same ciphertext plus non-secret tenant, client, request, message, expiry, and idempotency identifiers. - Cloudflare Queues deliver at least once. The message ID derives a stable, tenant-specific Resend idempotency key. Re-publishing a message reuses the exact stored ciphertext and key.
- The consumer claims only the matching tenant and message row, checks expiry before sending,
applies bounded retries, and records
sent,failed, orexpiredstatus. Permanent or exhausted failures may be copied to a configured dead-letter queue; those messages retain only the encrypted payload.
Provider and local adapters
Resend sends POST /emails requests with
the same Idempotency-Key for every retry, following its idempotency rules.
The provider adapter classifies network, rate-limit, server, and concurrent-idempotency errors as
retryable; other request errors are permanent. Provider error bodies and email contents are not
logged.
The local mailbox is in-memory, requires no provider secret, and deduplicates by idempotency key.
Use its findByRecipient method in local verification flows. Native, preview, and test setups must
provide the same tenant-bound payload cipher backed by @auth4/crypto.
Database migration
email_outbox is tenant-keyed and stores only ciphertext and delivery metadata. The migration
redacts legacy recipient values, fails unfinished legacy jobs that lack an encrypted payload, and
rejects future inserts that attempt to store a plaintext recipient. Deploy
infra/cloudflare/d1/migrations/0002_email_delivery_outbox.sql before enabling the email queue
consumer.
Source: docs/api-spec/auth4d-10.md