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

  1. The API creates a tenant-scoped outbox record before publishing a Queue message.
  2. Recipient address, verification code, product label, and message expiry are serialized and encrypted with the email:verification:delivery purpose 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.
  3. 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.
  4. The consumer claims only the matching tenant and message row, checks expiry before sending, applies bounded retries, and records sent, failed, or expired status. 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