Files
phaseflow/specs/notifications.md
Petru Paler 6a8d55c0b9 Document spec gaps: auth, phase scaling, observability, testing
Address 21 previously undefined behaviors across specs:

- Authentication: Replace email/password with OIDC (Pocket-ID)
- Cycle tracking: Add fixed-luteal phase scaling formula with examples
- Calendar: Document period logging behavior (preserve predictions)
- Garmin: Clarify connection is required (no phase-only mode)
- Dashboard: Add UI states, dark mode, onboarding, accessibility
- Notifications: Document timezone batching approach
- New specs: observability.md (health, metrics, logging)
- New specs: testing.md (unit + integration strategy)
- Main spec: Add backup/recovery, known limitations, API updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:49:56 +00:00

3.4 KiB

Notifications Specification

Job to Be Done

When I wake up each morning, I want to receive an email with my training decision, so that I don't need to open the app to know if I should train.

Email Notifications

Daily Training Email

Sent at user's preferred notificationTime (default: 07:00).

Subject:

PhaseFlow: [STATUS] - Day [cycleDay] ([phase])

Example: PhaseFlow: ✅ TRAIN - Day 12 (FOLLICULAR)

Body:

Good morning!

Today's Decision: [STATUS]
Reason: [reason]

Current Metrics:
- Cycle Day: [cycleDay] ([phase])
- Body Battery: [bbCurrent]
- HRV: [hrvStatus]
- Week Intensity: [weekIntensity]/[phaseLimit] min

Nutrition Today:
- Seeds: [seeds]
- Carbs: [carbRange]
- Keto: [ketoGuidance]

[Optional: Seed switch alert if day 15]
[Optional: Token expiration warning]

---
PhaseFlow - Training with your cycle, not against it

Token Expiration Warnings

14 Days Before: Subject: ⚠️ PhaseFlow: Garmin tokens expire in 14 days

7 Days Before: Subject: 🚨 PhaseFlow: Garmin tokens expire in 7 days - action required

Email Provider

Using Resend via resend npm package.

Configuration:

  • RESEND_API_KEY - API key for Resend
  • EMAIL_FROM - Sender address (e.g., PhaseFlow <noreply@phaseflow.app>)

Email Utilities (src/lib/email.ts)

Functions:

  • sendDailyNotification(user, decision, dailyData) - Send morning email
  • sendTokenExpirationWarning(user, daysUntilExpiry) - Send warning email

Cron Jobs

/api/cron/notifications

Protected by CRON_SECRET header.

Trigger: Daily at notification times

Process:

  1. Find all users with notificationTime matching current hour
  2. For each user:
    • Fetch current decision from decision engine
    • Send email via Resend
    • Update DailyLog.notificationSentAt

Timezone Handling

Users are batched into hourly timezone windows for efficient scheduling.

Implementation:

  1. Cron job runs 24 times daily (once per hour, on the hour)
  2. Each run queries users whose notificationTime hour matches the current UTC hour adjusted for their timezone
  3. Users are processed in batches within their timezone window

Example:

  • User A: timezone America/New_York, notificationTime 07:00
  • User B: timezone America/Los_Angeles, notificationTime 07:00
  • User C: timezone Europe/London, notificationTime 07:00

When cron runs at 12:00 UTC:

  • User A receives notification (12:00 UTC = 07:00 EST)
  • User B skipped (12:00 UTC = 04:00 PST)
  • User C skipped (12:00 UTC = 12:00 GMT)

Query Logic:

SELECT * FROM users
WHERE EXTRACT(HOUR FROM NOW() AT TIME ZONE timezone) = CAST(SUBSTRING(notificationTime, 1, 2) AS INT)

Rate Limiting

  • Max 1 notification per user per day
  • Check DailyLog.notificationSentAt before sending
  • Only send if null or different date

Success Criteria

  1. Email arrives within 5 minutes of scheduled time
  2. Email contains all relevant metrics and guidance
  3. Token warnings sent at 14 and 7 day thresholds
  4. No duplicate notifications on same day

Acceptance Tests

  • Daily email contains decision status and reason
  • Daily email includes nutrition guidance
  • Seed switch alert included on day 15
  • Token warning email sent at 14-day threshold
  • Token warning email sent at 7-day threshold
  • Duplicate notifications prevented
  • Timezone conversion correct for notification time
  • CRON_SECRET required for endpoint access