Email System Documentation
Email Templates & Purposes
1. Welcome Email
- Purpose: Sent to new users when they sign up
- Trigger: Clerk
user.createdwebhook event - Template:
GenerateWelcomeEmailHTML()ininternal/service/email/welcome_email.go - Subject: “👏 Welcome to Nümi: Your Wellness Adventure Begins!”
- Occurrence: Once per user (rate limited to 24 hours)
2. Badge Email
- Purpose: Sent when a user earns an achievement badge
- Trigger: First journey creation (
sendFirstJourneyBadgeIfNeeded()ininternal/api/journeys.go) - Template:
GenerateBadgeEmailHTML()ininternal/service/email/badge_email.go - Subject: “🏆 Achievement Unlocked: {BadgeName}!”
- Occurrence: When badge is earned (rate limited to 24 hours)
3. Check-in Reminder Email
- Purpose: Reminds users to complete their weekly check-ins
- Trigger: Background notification service (
NotificationService.checkForMissingCheckins()) - Template:
GenerateCheckinReminderHTML()ininternal/service/email/checkin_reminder_email.go - Subject: “Time for your wellness check-in! ❤️”
- Occurrence: Weekly, based on user’s check-in day and notification preferences
4. First Check-in Email
- Purpose: Celebrates the user’s first completed check-in
- Trigger: Level completion detection (
sendFirstCheckinEmail()ininternal/api/journeys.go) - Template:
GenerateFirstCheckinEmailHTML()ininternal/service/email/first_checkin_email.go - Subject: “🎉 Your First Check-in Complete! You’re Amazing!”
- Occurrence: Once per user (rate limited to 24 hours)
5. Level Completed Email
- Purpose: Congratulates users on completing a level
- Trigger: Level completion detection (
sendLevelCompletedEmail()ininternal/api/journeys.go) - Template:
GenerateLevelCompletedEmailHTML()ininternal/service/email/level_completed_email.go - Subject: “🎉 Level {N} Complete! Keep Going!”
- Occurrence: Each time a level is completed
6. Goal Reached Email
- Purpose: Celebrates reaching the target weight goal
- Trigger: Goal weight detection (
sendGoalReachedEmail()ininternal/api/journeys.go) - Template:
GenerateGoalReachedEmailHTML()ininternal/service/email/goal_reached_email.go - Subject: “🏆 Fantastic Job! You’ve Reached Your Goal!”
- Occurrence: When goal weight is reached (rate limited to 1 week)
7. Contact Form Email
- Purpose: Sends contact form submissions to support team
- Trigger: Contact form submission (
sendContactEmail()ininternal/api/contact.go) - Template:
GenerateContactEmailHTML()ininternal/service/email/contact_email.go - Subject: “[Nümi][Contact] {Subject} - {Name}”
- Occurrence: On each contact form submission
Email Evaluation & Sending Details
This section provides comprehensive information about when each email is evaluated and sent, including code locations, conditions, and timing.
1. Welcome Email
When Evaluated: Immediately when Clerk user.created webhook is received
Code Location:
- Handler: internal/handlers/clerk_webhooks.go → handleUserCreated() (line 181)
- Send Method: sendWelcomeEmailAsync() (line 237) → EmailService.SendWelcomeEmail()
Evaluation Flow:
Clerk Webhook → handleUserCreated() → sendWelcomeEmailAsync() →
Check rate limit (24h) → SendWelcomeEmail() → Record notification
Conditions:
1. User must have an email address in Clerk data
2. SMTP must be configured (IsEmailConfigured())
3. No welcome email sent in last 24 hours (HasRecentEmailNotification(userID, EmailTypeWelcome, 24))
Timing: Sent immediately after user creation (async goroutine, non-blocking)
Rate Limiting: 24 hours
2. Badge Email
When Evaluated: Immediately after journey creation
Code Location:
- Handler: internal/api/journeys.go → HandleJourneys() POST (line 82)
- Send Method: sendFirstJourneyBadgeIfNeeded() (line 579) → EmailService.SendBadgeEmail()
Evaluation Flow:
POST /api/journeys → HandleJourneys() → sendFirstJourneyBadgeIfNeeded() →
Check journey count (must be 1) → Check rate limit (24h) →
SendBadgeEmail() → Record notification
Conditions:
1. User must have exactly 1 journey (counted via CountUserJourneys())
2. User must have valid email in settings
3. SMTP must be configured
4. No badge email sent in last 24 hours
Timing: Sent immediately after first journey creation (async goroutine, non-blocking)
Rate Limiting: 24 hours
3. Check-in Reminder Email
When Evaluated: Continuously by background service
Code Location:
- Service: internal/service/notifications.go → NotificationService.checkForMissingCheckins() (see function checkForMissingCheckins)
- Service Start: NotificationService.Start() called in main.go
- Send Method: sendCheckinReminder() → EmailService.SendCheckinReminder() (see notifications.go)
Service Configuration:
- Check Interval: Configurable via DEFAULT_NOTIFICATION_CHECKPOINT_RATE (default: 1 minute)
- Email Time: Configurable via DEFAULT_NOTIFICATION_CHECKPOINT_TIME (default: “08:08”)
- Rate Limiting: Configurable via DEFAULT_NOTIFICATION_SPAM_CONTROL (default: 24 hours)
Evaluation Flow:
Background Service (every 1 min) → checkForMissingCheckins() →
Get all active journeys → Filter by user settings →
Check if check-in day → Check if email time →
Find missing check-ins → Check rate limit →
sendCheckinReminder() → SendCheckinReminder() → Record notification
Evaluation Conditions (ALL must be met):
1. ✅ User has notifications enabled (settings.NotificationsEnabled == true)
2. ✅ It’s the user’s check-in day (userDayOfWeek == settings.DefaultCheckinDay)
3. ✅ Current time matches email time in user’s timezone (isEmailTimeForUser())
4. ✅ User has active journey with missing check-ins:
- Level has EndWeight == 0 (no check-in recorded)
- Level StartDate is in the past or today
- Level status is not LevelCompleted
5. ✅ No check-in reminder sent within spam control interval (default: 24 hours)
Timing:
- Service runs: Continuously in background
- Checks performed: Every DEFAULT_NOTIFICATION_CHECKPOINT_RATE (default: 1 minute)
- Email sent: When current time in user’s timezone matches DEFAULT_NOTIFICATION_CHECKPOINT_TIME (default: 08:08)
- User timezone: Respects settings.TimeZone (default: America/New_York per model.DefaultTimeZone)
Rate Limiting: Configurable (default: 24 hours)
4. First Check-in Email
When Evaluated: When a journey is updated and a level is completed
Code Location:
- Handler: internal/api/journeys.go → checkAndSendLevelEmails() (line 655)
- Called From:
- HandleUpdateJourney() PUT handler (line 243)
- HandleUpdateJourneyLevel() PATCH handler (line 351)
- Send Method: sendFirstCheckinEmail() (line 725) → EmailService.SendFirstCheckinEmail()
Evaluation Flow:
PUT/PATCH /api/journeys → Update journey → checkAndSendLevelEmails() →
Detect newly completed level → Check if first completed →
sendFirstCheckinEmail() → Check rate limit (24h) →
SendFirstCheckinEmail() → Record notification
Conditions:
1. Level status changed to LevelCompleted
2. Level has EndWeight > 0
3. This is the first completed level for the user
4. No first check-in email sent in last 24 hours
Timing: Sent immediately when first level is completed (async goroutine, non-blocking)
Rate Limiting: 24 hours
5. Level Completed Email
When Evaluated: When a journey is updated and any level is completed
Code Location:
- Handler: internal/api/journeys.go → checkAndSendLevelEmails() (line 655)
- Called From:
- HandleUpdateJourney() PUT handler (line 243)
- HandleUpdateJourneyLevel() PATCH handler (line 351)
- Send Method: sendLevelCompletedEmail() (line 761) → EmailService.SendLevelCompletedEmail()
Evaluation Flow:
PUT/PATCH /api/journeys → Update journey → checkAndSendLevelEmails() →
Detect newly completed level → sendLevelCompletedEmail() →
SendLevelCompletedEmail() → Record notification
Conditions:
1. Level status changed to LevelCompleted
2. Level has EndWeight > 0
3. Level was not previously completed (comparison with old journey state)
Timing: Sent immediately when any level is completed (async goroutine, non-blocking)
Rate Limiting: None (sent for each completion)
6. Goal Reached Email
When Evaluated: When a journey is updated and goal weight is reached
Code Location:
- Handler: internal/api/journeys.go → checkAndSendLevelEmails() (line 655, lines 709-721)
- Called From:
- HandleUpdateJourney() PUT handler (line 243)
- HandleUpdateJourneyLevel() PATCH handler (line 351)
- Send Method: sendGoalReachedEmail() (line 792) → EmailService.SendGoalReachedEmail()
Evaluation Flow:
PUT/PATCH /api/journeys → Update journey → checkAndSendLevelEmails() →
Check if goal reached → Check if just reached →
sendGoalReachedEmail() → Check rate limit (1 week) →
SendGoalReachedEmail() → Record notification
Conditions:
1. Journey has TargetWeight > 0 and CurrentWeight > 0
2. Goal weight threshold crossed:
- If StartWeight > TargetWeight: CurrentWeight <= TargetWeight (weight loss)
- If StartWeight < TargetWeight: CurrentWeight >= TargetWeight (weight gain)
3. Journey status changed to StatusCompleted OR goal was just reached
4. No goal reached email sent in last 1 week
Timing: Sent immediately when goal is reached (async goroutine, non-blocking)
Rate Limiting: 1 week (168 hours)
7. Contact Form Email
When Evaluated: Immediately when contact form is submitted
Code Location:
- Handler: internal/api/contact.go → HandleContact() POST
- Send Method: sendContactEmailAsync() (goroutine) → sendContactEmail() → email.GenerateContactEmailHTML()
Evaluation Flow:
POST /api/contact → HandleContact() → Validate request →
go sendContactEmailAsync(req) → Return 200 immediately →
(async) sendContactEmail() → GenerateContactEmailHTML() → Send via gomail
Conditions: 1. Valid contact form submission 2. SMTP configured (checked inside async goroutine)
Timing: Queued immediately upon form submission (asynchronous, does not block request)
Rate Limiting: None
Email Service Architecture
Service Structure
- EmailService (
internal/service/emails.go): Core email sending service - NotificationService (
internal/service/notifications.go): Background service for check-in reminders - Email Templates (
internal/service/email/): All email HTML templates in dedicated folder
Synchronous vs Asynchronous
- All emails are sent asynchronously and do not block API or webhook responses:
- Contact Form:
go sendContactEmailAsync(req)inHandleContact() - Welcome:
go sendWelcomeEmailAsync(...)in Clerk user.created webhook - Badge / Level / First check-in / Goal:
go checkAndSendLevelEmails(...)orgo sendFirstJourneyBadgeIfNeeded(...)in journey handlers - Report:
go sendReportEmailAsync(...)in email-report handler - Check-in Reminder: background
NotificationServiceticker (not in request path)
- Contact Form:
- Test endpoint (
POST /api/test-emails) sends multiple templates synchronously by design for dev/testing.
Evaluation Timing Summary
- Immediate: Welcome, Badge, First Check-in, Level Completed, Goal Reached, Contact Form
- Scheduled: Check-in Reminder (background service with time-based triggers)
Rate Limiting Summary
| Email Type | Rate Limit | Check Method |
|---|---|---|
| Welcome | 24 hours | HasRecentEmailNotification(userID, EmailTypeWelcome, 24) |
| Badge | 24 hours | HasRecentEmailNotification(userID, EmailTypeBadgeAward, 24) |
| Check-in Reminder | Configurable (default: 24h) | HasRecentEmailNotification(userID, EmailTypeCheckinReminder, spamControl) |
| First Check-in | 24 hours | HasRecentEmailNotification(userID, EmailTypeFirstCheckin, 24) |
| Level Completed | None | N/A (sent for each completion) |
| Goal Reached | 1 week (168h) | HasRecentEmailNotification(userID, EmailTypeGoalReached, 168) |
| Contact Form | None | N/A |
Database Tracking
- All emails are recorded in
email_notificationscollection - Used for rate limiting and tracking
- Includes: UserID, EmailType, JourneyID, Subject, CreatedAt
Error Handling
- All email sending errors are logged but don’t block the main operation
- Rate limiting failures are logged but don’t prevent future attempts
- SMTP configuration checks prevent sending when not configured
Email Configuration
Environment Variables Required:
SMTP_HOST- SMTP server hostnameSMTP_PORT- SMTP server port (default: 587)SMTP_USER- SMTP usernameSMTP_PASS- SMTP passwordSMTP_FROM_EMAIL- From email addressSMTP_FROM_NAME- From name (optional)
Notification Service Environment Variables:
DEFAULT_NOTIFICATION_CHECKPOINT_TIME- Time to send check-in reminder emails (default: “08:08”)DEFAULT_NOTIFICATION_CHECKPOINT_RATE- How often to check for missing check-ins (default: “1m”)DEFAULT_NOTIFICATION_SPAM_CONTROL- Hours between reminder emails (default: 24)
Email Service Location:
- Service:
internal/service/emails.go - Initialization:
service.NewEmailService()inmain.go - Library:
gopkg.in/gomail.v2 - Templates:
internal/service/email/(dedicated package)
Test Endpoint
Test Email Endpoint:
- Route:
/api/test-emails(POST) - Location:
internal/api/test_emails.go - Behavior: Sends all 7 email template samples to the specified destination email
- Purpose: Allows reviewing all email template content, layout, and design
Simple Email Test
Send a POST request to /api/test-emails with JSON body:
{
"email": "your-email@example.com"
}
Example using curl:
curl -X POST http://localhost:8080/api/test-emails \
-H "Content-Type: application/json" \
-d '{"email": "your-email@example.com"}'
Response on success (all emails sent):
{
"success": true,
"message": "Test emails sent to your-email@example.com",
"email": "your-email@example.com",
"total_sent": 7,
"successful": 7,
"failed": 0,
"results": {
"welcome": "Sent successfully",
"badge": "Sent successfully",
"checkin_reminder": "Sent successfully",
"first_checkin": "Sent successfully",
"level_completed": "Sent successfully",
"goal_reached": "Sent successfully",
"contact_form": "Sent successfully"
},
"email_types": {
"welcome": "Welcome Email - Sent to new users when they sign up",
"badge": "Badge Email - Sent when a user earns an achievement badge",
"checkin_reminder": "Check-in Reminder Email - Reminds users to complete their weekly check-ins",
"first_checkin": "First Check-in Email - Celebrates the user's first completed check-in",
"level_completed": "Level Completed Email - Congratulates users on completing a level",
"goal_reached": "Goal Reached Email - Celebrates reaching the target weight goal",
"contact_form": "Contact Form Email - Sends contact form submissions to support team"
}
}
Response on partial failure:
{
"success": false,
"message": "Test emails sent to your-email@example.com",
"email": "your-email@example.com",
"total_sent": 7,
"successful": 5,
"failed": 2,
"results": {
"welcome": "Sent successfully",
"badge": "Sent successfully",
"checkin_reminder": "Failed: SMTP connection timeout",
"first_checkin": "Sent successfully",
"level_completed": "Sent successfully",
"goal_reached": "Sent successfully",
"contact_form": "Failed: invalid email address"
},
"email_types": { ... }
}
Note: The endpoint sends all 7 email types sequentially with a 1-second delay between each email to avoid overwhelming the SMTP server. Check your inbox (and spam folder) for all email samples.
Potential Issues
Common Email Delivery Problems:
- SMTP configuration missing or incorrect
- SMTP authentication failures
- Email service not initialized properly
- Network/firewall blocking SMTP connections
- Email provider rate limiting
- Invalid email addresses
- Email marked as spam
- Template generation errors
Debug Hypotheses
Hypothesis A: SMTP Configuration Missing or Incorrect
- Description: Environment variables (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL) are not set or contain incorrect values
- Evidence to check: Debug logs showing empty or invalid SMTP configuration values
- Location:
NewEmailService()initialization
Hypothesis B: SMTP Configuration Check Failing
- Description:
IsEmailConfigured()returns false even when some values are set - Evidence to check: Debug logs showing configuration check failure despite values being present
- Location:
sendEmail()function
Hypothesis C: SMTP Port Parsing Issue
- Description: Port number is not being parsed correctly from environment variable
- Evidence to check: Debug logs showing incorrect port value or default port being used
- Location:
sendEmail()port parsing logic
Hypothesis D: Email Message Creation Failure
- Description: Email message headers or body are not being set correctly
- Evidence to check: Debug logs showing message creation but missing headers
- Location:
sendEmail()message creation
Hypothesis E: SMTP Connection Failure
- Description: Cannot establish connection to SMTP server (network, firewall, or server down)
- Evidence to check: Debug logs showing connection attempt but no success
- Location:
DialAndSend()call
Hypothesis F: SMTP Authentication Failure
- Description: SMTP server rejects credentials (wrong username/password)
- Evidence to check: Debug logs showing connection established but authentication error
- Location:
DialAndSend()call
Hypothesis G: Email Sent but Not Received
- Description: Email is successfully sent from SMTP server but not delivered to recipient
- Evidence to check: Debug logs showing successful send but user reports no email
- Location: After
DialAndSend()success