Email Templates
This document describes the email template system for developers who need to update existing templates or add new ones.
Overview
The email system uses:
- Handlebars for template rendering with variables and partials
- AWS SES for email delivery
- RDS Data API for email logging (no VPC required)
- File-based templates for easy updates without code changes
Architecture
backend/shared/email/
├── templates/
│ ├── _partials/ # Reusable components
│ │ ├── header.html # Logo and header styling
│ │ ├── footer.html # Links and unsubscribe
│ │ └── styles.html # CSS styling
│ ├── welcome/
│ │ ├── subject.txt # Email subject line
│ │ ├── template.html # HTML version
│ │ └── template.txt # Plain text version
│ ├── enrollment-confirmation/
│ └── course-completion/
├── services/
│ └── email-service.ts # Main email service
├── types/
│ └── email.types.ts # Type definitions
└── index.ts # Module exports
Existing Templates
1. Welcome Email
Template: backend/shared/email/templates/welcome/
Trigger: Cognito Post-Confirmation (user confirms email after signup)
Business Process:
- User signs up and confirms email
- Cognito triggers Post-Confirmation Lambda
- User record created in database
- Welcome email sent asynchronously
Trigger Location: backend/functions/auth/post-confirmation/index.ts
Template Variables:
| Variable | Type | Description |
|---|---|---|
firstName |
string | User’s first name |
loginUrl |
string | Link to sign-in page |
email |
string | Recipient email |
frontendUrl |
string | Frontend base URL |
Subject: Welcome to Momentum, !
2. Enrollment Confirmation Email
Template: backend/shared/email/templates/enrollment-confirmation/
Trigger: User enrolls in a course via POST /enrollments
Business Process:
- User clicks “Enroll” on course page
- EnrollmentService creates enrollment record
- Confirmation email sent asynchronously
Trigger Location: backend/shared/services/EnrollmentService.ts (line 237-289)
Template Variables:
| Variable | Type | Description |
|---|---|---|
firstName |
string | User’s first name |
courseName |
string | Name of the course |
courseDescription |
string | Course description |
courseDuration |
string | Duration (e.g., “30 days”) |
startDate |
string | Enrollment date (formatted) |
courseUrl |
string | Link to course/dashboard |
frontendUrl |
string | Frontend base URL |
Subject: You're enrolled in !
3. Course Completion Email
Template: backend/shared/email/templates/course-completion/
Trigger: User completes all lessons in a course
Business Process:
- User marks final lesson complete
- ProgressService checks if all lessons done
- Enrollment status updated to COMPLETED
- Completion email sent asynchronously
Trigger Location: backend/shared/services/ProgressService.ts (line 120, 146)
Template Variables:
| Variable | Type | Description |
|---|---|---|
firstName |
string | User’s first name |
courseName |
string | Name of completed course |
completionDate |
string | Completion date (formatted) |
certificateUrl |
string | (Optional) Link to certificate |
nextCourseRecommendations |
array | (Optional) Recommended courses |
frontendUrl |
string | Frontend base URL |
Subject: Congratulations! You've completed
Adding a New Template
Step 1: Create Template Files
Create a new folder in backend/shared/email/templates/:
mkdir backend/shared/email/templates/my-new-template
Create three required files:
subject.txt - Single line subject with optional Handlebars variables:
Your order #{{orderId}} has been confirmed
template.html - HTML email with partials:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order Confirmation</title>
{{> styles}}
</head>
<body>
<div class="container">
{{> header}}
<div class="content">
<h1>Thank you, {{firstName}}!</h1>
<p>Your order #{{orderId}} has been confirmed.</p>
{{#if items}}
<ul>
{{#each items}}
<li>{{this.name}} - ${{this.price}}</li>
{{/each}}
</ul>
{{/if}}
<div class="cta">
<a href="{{orderUrl}}" class="button">View Order</a>
</div>
</div>
{{> footer}}
</div>
</body>
</html>
template.txt - Plain text fallback:
Thank you, {{firstName}}!
Your order #{{orderId}} has been confirmed.
View your order: {{orderUrl}}
---
Momentum Learning Platform
Step 2: Register the Template
Update backend/shared/email/types/email.types.ts:
export const EMAIL_TEMPLATES = [
'welcome',
'enrollment-confirmation',
'course-completion',
'my-new-template', // Add your template
] as const;
Step 3: Update Template Loader
Update backend/shared/email/templates/index.ts:
export interface Templates {
welcome: TemplateFiles;
'enrollment-confirmation': TemplateFiles;
'course-completion': TemplateFiles;
'my-new-template': TemplateFiles; // Add type
}
export const templates: Templates = {
'welcome': loadTemplate('welcome'),
'enrollment-confirmation': loadTemplate('enrollment-confirmation'),
'course-completion': loadTemplate('course-completion'),
'my-new-template': loadTemplate('my-new-template'), // Add loader
};
Step 4: Add Trigger Code
Invoke the email Lambda from your service/handler:
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
const lambdaClient = new LambdaClient({ region: process.env.AWS_REGION });
async function sendMyNewEmail(userId: string, data: MyEmailData) {
const emailLambdaName = process.env.EMAIL_LAMBDA_NAME;
if (!emailLambdaName) {
console.warn('EMAIL_LAMBDA_NAME not configured, skipping email');
return;
}
try {
await lambdaClient.send(
new InvokeCommand({
FunctionName: emailLambdaName,
InvocationType: 'Event', // Async - fire and forget
Payload: JSON.stringify({
template: 'my-new-template',
to: data.email,
userId: userId,
data: {
firstName: data.firstName,
orderId: data.orderId,
orderUrl: `${process.env.FRONTEND_URL}/orders/${data.orderId}`,
items: data.items,
},
}),
})
);
console.log('Email queued successfully');
} catch (error) {
// Log but don't fail the main operation
console.error('Failed to queue email:', error);
}
}
Step 5: Deploy
# Rebuild and deploy the email Lambda
./scripts/deployment/deploy-backend.sh
# Or deploy all
./scripts/deployment/deploy-all.sh
Updating Existing Templates
Modifying Content
Edit the template files directly:
# Edit HTML template
vim backend/shared/email/templates/welcome/template.html
# Edit subject line
vim backend/shared/email/templates/welcome/subject.txt
# Edit plain text version
vim backend/shared/email/templates/welcome/template.txt
After editing, redeploy the email Lambda:
./scripts/deployment/deploy-backend.sh
Adding New Variables
- Update the template files to use the new variable:
<p>Welcome, {{firstName}}! Your company: {{companyName}}</p>
- Update the trigger code to pass the new variable:
data: {
firstName: user.firstName,
companyName: user.companyName, // Add new variable
// ...
}
Template Syntax Reference
Variables
Conditionals
Loops
Partials (Reusable Components)
Partials Reference
_partials/header.html
Contains the Momentum logo and header styling. Used at the top of every email.
_partials/footer.html
Contains:
- Links to Email Preferences, Support, Dashboard
- Unsubscribe option
- Company address and copyright
_partials/styles.html
CSS styling including:
- Brand colors (purple/blue gradient)
- Button styling
- Responsive design
- Typography
Email Service API
Sending Emails Programmatically
import { EmailService } from '@/shared/email';
const result = await EmailService.send({
template: 'welcome',
to: 'user@example.com',
userId: 'uuid-here',
data: {
firstName: 'John',
loginUrl: 'https://momentum.cloudnnj.com/sign-in',
},
});
if (result.success) {
console.log('Email sent:', result.messageId);
} else {
console.error('Email failed:', result.error);
}
Payload Structure
interface EmailPayload {
template: EmailTemplate; // Template name
to: string; // Recipient email
data: Record<string, unknown>; // Template variables
userId?: string; // For logging/tracking
replyTo?: string; // Optional reply-to
}
Email Logging
All emails are logged to the email_logs table:
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | Associated user |
template_name |
VARCHAR | Template used |
subject |
VARCHAR | Rendered subject |
recipient_email |
VARCHAR | Recipient |
status |
VARCHAR | pending/sent/delivered/bounced/complained/failed |
ses_message_id |
VARCHAR | SES tracking ID |
error_message |
TEXT | Error details (if failed) |
retry_count |
INTEGER | Retry attempts |
metadata |
JSONB | Additional data |
sent_at |
TIMESTAMP | When sent |
created_at |
TIMESTAMP | When logged |
Query Email Logs
./scripts/database/db-query-remote.sh "
SELECT template_name, status, COUNT(*)
FROM email_logs
GROUP BY template_name, status
"
Configuration
Environment Variables
| Variable | Description |
|---|---|
SES_FROM_EMAIL |
Sender email address |
SES_FROM_NAME |
Sender display name |
SES_CONFIGURATION_SET |
SES config set for tracking |
FRONTEND_URL |
Base URL for links |
EMAIL_LAMBDA_NAME |
Lambda function name (for invocation) |
Email Settings Table
System-wide settings in email_settings table:
| Setting | Default | Description |
|---|---|---|
welcome_email_enabled |
true | Send welcome emails |
enrollment_confirmation_enabled |
true | Send enrollment confirmations |
completion_email_enabled |
true | Send completion emails |
learning_reminders_enabled |
true | Send learning reminders |
reminder_frequency_days |
1 | Days between reminders |
reminder_time |
09:00 | Time to send reminders |
Testing
Unit Tests
cd backend
npx jest shared/email --coverage
Manual Testing
- Use a verified email in SES sandbox mode
- Trigger the email (signup, enroll, complete course)
- Check CloudWatch logs for the email Lambda
- Verify email received and rendering
Test Email Locally
// In a test file or script
import { EmailService } from '@/shared/email';
const result = await EmailService.send({
template: 'welcome',
to: 'verified-email@example.com',
data: {
firstName: 'Test',
loginUrl: 'https://momentum.cloudnnj.com/sign-in',
},
});
console.log(result);
Troubleshooting
Email Not Sending
- Check CloudWatch logs for the email Lambda
- Verify
EMAIL_LAMBDA_NAMEis set in the calling Lambda - Confirm SES is out of sandbox mode (or recipient is verified)
- Check email_logs table for error messages
Template Not Found
- Verify template folder exists in
backend/shared/email/templates/ - Check all three files exist:
subject.txt,template.html,template.txt - Confirm template is registered in
email.types.tsandtemplates/index.ts
Variables Not Rendering
- Check variable names match between template and data payload
- Ensure data is passed correctly to the email service
- Use
insideloops
Future Templates (Phase 2+)
Planned templates not yet implemented:
lesson-reminder- Remind users about pending lessonsinactivity-reminder- Re-engage inactive usersweekly-digest- Weekly progress summarynew-course-announcement- Notify about new courses
Related Files
| File | Description |
|---|---|
backend/shared/email/services/email-service.ts |
Main email service |
backend/shared/email/types/email.types.ts |
Type definitions |
backend/shared/email/templates/index.ts |
Template loader |
backend/functions/email/src/index.ts |
Email Lambda handler |
backend/functions/auth/post-confirmation/index.ts |
Welcome email trigger |
backend/shared/services/EnrollmentService.ts |
Enrollment email trigger |
backend/shared/services/ProgressService.ts |
Completion email trigger |
infrastructure/terraform/lambda-email.tf |
Email Lambda Terraform |
backend/migrations/020_create_email_logs.sql |
Email logs schema |