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:

  1. User signs up and confirms email
  2. Cognito triggers Post-Confirmation Lambda
  3. User record created in database
  4. 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:

  1. User clicks “Enroll” on course page
  2. EnrollmentService creates enrollment record
  3. 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:

  1. User marks final lesson complete
  2. ProgressService checks if all lessons done
  3. Enrollment status updated to COMPLETED
  4. 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

  1. Update the template files to use the new variable:
<p>Welcome, {{firstName}}! Your company: {{companyName}}</p>
  1. Update the trigger code to pass the new variable:
data: {
  firstName: user.firstName,
  companyName: user.companyName,  // Add new variable
  // ...
}

Template Syntax Reference

Variables

{{variableName}}

Conditionals

{{#if certificateUrl}}
  <a href="{{certificateUrl}}">Download Certificate</a>
{{else}}
  <p>Certificate coming soon!</p>
{{/if}}

Loops

{{#each items}}
  <li>{{this.name}} - {{this.price}}</li>
{{/each}}

Partials (Reusable Components)

{{> header}}
{{> footer}}
{{> styles}}

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

  1. Use a verified email in SES sandbox mode
  2. Trigger the email (signup, enroll, complete course)
  3. Check CloudWatch logs for the email Lambda
  4. 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

  1. Check CloudWatch logs for the email Lambda
  2. Verify EMAIL_LAMBDA_NAME is set in the calling Lambda
  3. Confirm SES is out of sandbox mode (or recipient is verified)
  4. Check email_logs table for error messages

Template Not Found

  1. Verify template folder exists in backend/shared/email/templates/
  2. Check all three files exist: subject.txt, template.html, template.txt
  3. Confirm template is registered in email.types.ts and templates/index.ts

Variables Not Rendering

  1. Check variable names match between template and data payload
  2. Ensure data is passed correctly to the email service
  3. Use inside loops

Future Templates (Phase 2+)

Planned templates not yet implemented:

  • lesson-reminder - Remind users about pending lessons
  • inactivity-reminder - Re-engage inactive users
  • weekly-digest - Weekly progress summary
  • new-course-announcement - Notify about new courses

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

Back to top

Momentum LMS © 2025. Distributed under the MIT license.