Repository navigation
Description
Goal
Build a reusable email notification foundation using Cloudflare Email Service. Digests, important notifications, and daily learning/streak reminders should later plug into the same preferences, scheduling, templates, and delivery pipeline.
These three user-configurable categories are enabled by default. Users can disable each independently or turn off optional email altogether. Implement the foundation first; enabling category preferences must not silently launch unfinished features or replay historical notifications.
Existing DevFeed building blocks
apps/notifications/src/delivery.pydelivers durable notification jobs through Chimely with leases, retries and separate user/admin audiences.packages/core/src/notification_delivery.pyowns recovery and a retry window based specifically on Chimely's idempotency retention. That guarantee must not be assumed for Cloudflare.UserAccount.notification_settingsandNotificationSettingscurrently expose badge/sound preferences, not email subscriptions or verified email eligibility.feed_notifications.pyalready rechecks audience/content eligibility before delivery; reuse this principle for email.reading_streaks.pyrecords distinct article opens against UTC days, not local-midnight deadlines or proof of learning completion. Reminder wording and scheduling must reflect the actual streak contract.
Preserve existing in-app notifications and admin/user boundaries. Add a channel-specific email delivery path to the existing durable job architecture; avoid replacing Chimely or introducing a second general notification platform.
Research findings and provider decision
Cloudflare Email Sending is currently beta and supports outbound transactional email through REST, Workers bindings, or SMTP. A Python backend can call its REST endpoint directly, without moving the application to Workers. Sending to arbitrary user addresses requires Workers Paid and an onboarded sending domain in Cloudflare DNS. Overview, domain setup.
Prefer a small Cloudflare REST adapter in the existing delivery service. Before finalizing it, prove how sends correlate to later delivery events: documented REST responses group recipients by delivery outcome, whereas the Workers binding exposes a message ID. Do not assume identical response schemas or undocumented provider idempotency. If reliable correlation requires a thin Worker using the binding, document that tradeoff before adding one. REST contract.
Cloudflare supports transactional email only, excluding marketing. Confirm that the precise personalized digest and learning-reminder use cases qualify before launching those features; an API example mentioning a digest is not blanket approval for promotional newsletters. Keep sponsorships, promotional campaigns and bulk marketing outside this transport. Provider FAQ.
Delivery/defer/bounce/failure/rejection events are available through domain-scoped event subscriptions to Cloudflare Queues, with Workers or authenticated HTTP-pull consumers. Do not invent a provider webhook contract. A recipient server accepting a message does not prove inbox placement or that the user read it. Event subscriptions.
Foundation scope
1. Categories, preferences and recipient identity
- Stable category registry, initially reserving
digest,important, andlearning_reminder; separate event payload schemas from templates and provider code. - Master optional-email toggle plus independent category switches, all enabled by default. Preserve explicit choices through migrations, new category additions and account updates; never reset an unsubscribe.
- Configurable digest cadence, reminder delivery time, IANA timezone, quiet hours and a global optional-email frequency cap. Pick and document initial defaults when implementing the settings UI.
- Show defaults clearly in account settings/onboarding. Apply them consistently to existing and new accounts without queuing a historical backlog.
- Send only to a verified, current account email. Establish trusted verification evidence from OIDC or a separate verification flow; the presence of an email string alone is insufficient. Account deletion, address changes and loss of verification invalidate pending delivery to the old address.
- Keep optional product messages separate from any future essential security/account notices. Do not route operational/admin messages into reader email audiences.
- Settings must work on the web and in both Chrome and Edge reader extensions.
2. Durable delivery and scheduling
- Transactional outbox intent with category, user identity, event/window key, template version, scheduling time, expiry, status and attempts. One recipient per personalized email; never expose other users through To/CC.
- Database uniqueness for logical deliveries, leases and crash recovery. Keep network calls outside application transactions. Reuse existing job primitives without assuming Chimely's 28-day retry policy is suitable for email.
- Explicit lifecycle distinguishing scheduled/queued, sending, provider accepted/queued, delivered, deferred, bounced, rejected, failed, cancelled, suppressed and unknown outcome.
- Retry transient errors with bounded exponential backoff/jitter and provider rate-limit hints. Do not retry invalid recipients, hard bounces or suppression/policy rejection indefinitely.
- Prove handling for a timeout after possible provider acceptance. Local uniqueness alone cannot guarantee exactly-once external sending; reconcile or hold ambiguous attempts rather than blindly resend.
- Do not create another send because Cloudflare has deferred an already accepted message. Separate submission retries from provider delivery retries.
- Recheck account status, current address, verification, preferences, suppression, content eligibility, expiry and category-specific conditions immediately before submission.
- Scheduling handles missed runs, clock/timezone changes, DST and concurrent workers without duplicates. Expired reminders should be skipped instead of delivered late. Throttle catch-up after outages.
3. Preferences and unsubscribe
- Every optional email includes a readable preferences link and unsubscribe path. Support category unsubscribe and stop-all-optional-email actions without requiring login.
- Use signed opaque action tokens, no email addresses or credentials in URLs. Link-scanner GET requests must not unsubscribe users accidentally; implement one-click POST semantics separately from a human confirmation page.
- Include appropriate
List-UnsubscribeandList-Unsubscribe-Postheaders. Apply an opt-out before queued sends/retries can proceed. Keep category opt-outs separate from address-wide delivery suppression. - No open-tracking pixels by default. Email fetching, link previews and scanners must never count as learning activity or extend a streak.
Cloudflare allows list-management headers and custom X-headers, but some headers are provider-controlled. Verify supported headers rather than overriding generated Message-ID behavior. Header contract.
4. Templates and content
- Shared responsive HTML and plain-text layouts, versioned per category, with accessible typography, dark-mode checks, clear sender identity and a useful reply/support destination.
- Escape user/article text; use trusted canonical links and a real unsubscribe footer. Avoid leaking private account data through previews, public routes, logs or external images.
- Template previews and fixture-based rendering tests, safe test recipients and non-production capture mode. Localization-ready content and language fallback without requiring a translation platform initially.
5. Deliverability and provider events
- Onboard the sending domain/subdomain; verify SPF, DKIM and DMARC alignment without disrupting existing mailbox DNS. Configure a monitored reply/support address.
- Consume provider events with authenticated access, deduplicate event IDs, handle out-of-order events and retain correlation evidence. Do not regress terminal states when a delayed event arrives.
- Respect Cloudflare account/domain suppressions and keep appropriate local suppression state. Process eligible bounces and complaints using the documented event/API mechanisms; reconcile when the provider does not publish a separate event for a change.
- Never auto-clear complaint/hard-bounce suppression just because a user enables a category.
- Model quotas, backpressure and spending limits. Keep limits configurable because Cloudflare adjusts daily quotas by account reputation. Estimate volume from active recipients × cadence before default-on feature rollout.
Suppression behavior, sending limits, current pricing.
6. Operations and validation
- Admin visibility into delivery state, retry timing, safe failure reasons, suppression and provider correlation. Search by user/category/event; keep message bodies and email addresses out of routine logs and metrics labels.
- Metrics for queue age, submission errors, provider deferrals, deliveries, bounces, suppressions, unsubscribe rate and volume/cost. Alert on a stuck queue or sudden failure/bounce growth.
- Global and per-category kill switches, controlled rollout, internal recipient allowlist and quota enforcement. Turning sending back on must not flood users with stale mail.
- Document secrets, least-privilege Cloudflare credentials, configuration, migrations, event-consumer setup, retention/deletion and recovery procedures.
Future features built on this foundation
These are follow-up implementations, not a requirement to ship all three in the first foundation PR:
| Category | Intended behavior | Required eligibility rules |
|---|---|---|
| Digest | Personalized daily/weekly roundup from followed topics, sources, tags or recommendations; configurable cadence | Published, eligible content; suppress empty digests; stable time-window deduplication; no repeated items across retries |
| Important notifications | Defined high-value user/account events with relevant deep links | Explicit event allowlist and priority; configurable category; dedupe against the event; no arbitrary "urgent" label to evade preferences |
| Learning/streak reminder | Helpful reminder to read when the user has not yet recorded qualifying activity | At most one per applicable streak day; cancel if activity occurs before send; never claim a local-midnight deadline while streak accounting remains UTC; email opens do not award activity |
Reminders can use the user's chosen delivery timezone, but streak-day identity and expiry must stay consistent with current UTC accounting. Changing the streak model itself requires an explicit follow-up migration/product decision.
Acceptance criteria
- Shared category, recipient, preference, template and delivery contracts are documented; future categories can plug in without copying transport/retry code.
- All three category defaults and the master optional-email switch are persisted and visible; explicit opt-outs survive migrations and queued retries.
- A controlled end-to-end test message reaches a verified internal recipient using Cloudflare, with plain text/HTML, preferences links and a traceable provider outcome.
- Tests cover rollback/outbox atomicity, duplicate events, concurrent claims, lease loss, ambiguous submission outcomes, provider deferral, rate limits, permanent failure, suppression, unsubscribe and account/address changes.
- Event ingestion is authenticated and idempotent, with correlation and out-of-order behavior tested against the actual provider contract.
- Scheduling tests cover DST/timezone changes, outage recovery, expiry, frequency limits and activity arriving just before a reminder sends.
- Existing in-app notification behavior and audience separation remain covered; web and both built extensions are verified for preferences changes.
- Admin diagnostics, kill switches, safe preview mode and staged rollout are ready before user-wide sending is enabled.
- Digest, important-event selection and reminder feature work is split into linked follow-up issues with the foundation as a dependency; provider-use eligibility is resolved before those launches.
Research checked against Cloudflare documentation on 20 October 2026. Account entitlement, DNS setup and actual quotas have not been inspected or changed. This issue does not authorize sending production email or enabling future campaigns.
Activity