24 KiB
Email & Notification Service
Complete documentation for the Tour Builder Platform's email and notification system including Nodemailer/SES integration, verification emails, and invitations.
Overview
The platform implements a transactional email system using Nodemailer with AWS SES (Simple Email Service) for reliable email delivery. The system handles user verification, password resets, and team invitations.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Email Service Architecture │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Email Triggers │ │
│ │ │ │
│ │ User Signup ──────────┐ │ │
│ │ Password Reset ───────┼──> AuthService ──> EmailSender ──> AWS SES │ │
│ │ User Invitation ──────┘ │ │
│ │ Email Verification ───────────────────────────────────────────────> │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Email Templates │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Address │ │ Password │ │ Invitation │ │ │
│ │ │ Verification │ │ Reset │ │ │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Configuration
Environment Variables
| Variable | Description | Required |
|---|---|---|
EMAIL_USER |
SMTP username (AWS SES SMTP credentials) | Yes |
EMAIL_PASS |
SMTP password (AWS SES SMTP credentials) | Yes |
EMAIL_TLS_REJECT_UNAUTHORIZED |
TLS certificate validation (true/false) |
No (default: true) |
Config Settings
Source: backend/src/config.ts
email: {
from: 'Tour Builder Platform <app@flatlogic.app>',
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
auth: {
user: process.env.EMAIL_USER || '',
pass: process.env.EMAIL_PASS,
},
tls: {
rejectUnauthorized: process.env.EMAIL_TLS_REJECT_UNAUTHORIZED !== 'false',
}
}
AWS SES Configuration
The system uses AWS SES SMTP interface in us-east-1 region:
- Host:
email-smtp.us-east-1.amazonaws.com - Port: 587 (TLS)
- Configuration Set:
flatlogic-app(for tracking/analytics)
Email Service Class
Source: backend/src/services/email/index.ts
import nodemailer from 'nodemailer';
export default class EmailSender {
constructor(private readonly email: EmailTemplate) {}
async send(): Promise<EmailSendResult> {
// Validates: email, to, subject, html
const htmlContent = await this.email.html();
const transporter = nodemailer.createTransport(this.transportConfig);
const mailOptions = {
from: this.from,
to: this.email.to,
subject: this.email.subject,
html: htmlContent,
headers: {
'X-SES-CONFIGURATION-SET': 'flatlogic-app',
},
};
return transporter.sendMail(mailOptions);
}
static get isConfigured(): boolean {
return Boolean(config.email.auth.pass && config.email.auth.user);
}
}
Configuration Check
The EmailSender.isConfigured static property checks if email credentials are set. When email is not configured:
- Email verification is skipped when email delivery is disabled
- Users are automatically marked as verified
- Password reset/invitation emails are not sent
Email Types
1. Email Address Verification
Source: backend/src/services/email/list/addressVerification.ts
Triggered by:
- Manual verification request (
POST /api/auth/send-email-address-verification-email)
Template: backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html
Template Variables:
| Variable | Description |
|---|---|
{appTitle} |
Application name ("Tour Builder Platform") |
{signupUrl} |
Verification link with token |
Note: The code attempts to replace
{to}but this placeholder is not present in the HTML template.
Token Generation:
// UsersDBApi._generateToken
const token = crypto.randomBytes(20).toString('hex');
const tokenExpiresAt = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
// Stored in users table:
// - emailVerificationToken
// - emailVerificationTokenExpiresAt
2. Password Reset
Source: backend/src/services/email/list/passwordReset.ts
Triggered by:
- Password reset request (
POST /api/auth/send-password-reset-email)
Template: backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html
Template Variables:
| Variable | Description |
|---|---|
{appTitle} |
Application name |
{resetUrl} |
Password reset link with token |
{accountName} |
User's email address |
Token Generation:
// Stored in users table:
// - passwordResetToken
// - passwordResetTokenExpiresAt (24 hours)
3. User Invitation
Source: backend/src/services/email/list/invitation.ts
Triggered by:
- User creation with
sendInvitationEmails: true - Bulk user import
Template: backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html
Template Variables:
| Variable | Description |
|---|---|
{appTitle} |
Application name |
{signupUrl} |
Invitation link (password reset link with &invitation=true) |
Note: The code attempts to replace
{to}but this placeholder is not present in the HTML template.
Usage in UsersService:
// backend/src/services/users.ts
if (emailsToInvite && emailsToInvite.length) {
AuthService.sendPasswordResetEmail(email, 'invitation', host);
}
HTML Template Structure
All email templates follow a consistent structure:
<!DOCTYPE html>
<html>
<head>
<style>
.email-container { max-width: 600px; margin: auto; }
.email-header { background-color: #3498db; color: #fff; padding: 16px; }
.email-body { padding: 16px; }
.email-footer { background-color: #f7fafc; padding: 16px; }
.btn-primary { background-color: #3498db; color: #fff; padding: 8px 16px; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">Welcome to {appTitle}!</div>
<div class="email-body"><!-- Content --></div>
<div class="email-footer">Thanks, The {appTitle} Team</div>
</div>
</body>
</html>
Notification System
Message Lookup
Source: backend/src/services/notifications/helpers.js
The notification system uses a key-based lookup for all user-facing messages:
const { getNotification } = require('./notifications/helpers');
// Usage
getNotification('emails.invitation.subject', appTitle);
// Returns: "You've been invited to Tour Builder Platform"
Message Catalog
Source: backend/src/services/notifications/list.ts
| Key | Message |
|---|---|
app.title |
"Tour Builder Platform" |
emails.invitation.subject |
"You've been invited to {0}" |
emails.emailAddressVerification.subject |
"Verify your email for {0}" |
emails.passwordReset.subject |
"Reset your password for {0}" |
auth.userNotVerified |
"Sorry, your email has not been verified yet" |
auth.emailAddressVerificationEmail.invalidToken |
"Email verification link is invalid or has expired" |
auth.passwordReset.invalidToken |
"Password reset link is invalid or has expired" |
Note: The message catalog also contains
emails.*.bodytext templates, but these are not used. The system reads HTML templates from thehtmlTemplates/directory instead.
API Endpoints
Email-Related Auth Endpoints
| Method | Endpoint | Description | Rate Limit |
|---|---|---|---|
POST |
/api/auth/signin/local |
User login | 10/15min |
POST |
/api/auth/send-email-address-verification-email |
Resend verification (auth required) | - |
PUT |
/api/auth/verify-email |
Verify email token | - |
POST |
/api/auth/send-password-reset-email |
Send password reset | 5/hour |
PUT |
/api/auth/password-reset |
Reset password with token | - |
PUT |
/api/auth/password-update |
Change password (auth required) | - |
GET |
/api/auth/email-configured |
Check if email is configured | - |
Rate Limiting
Source: backend/src/middlewares/rateLimiter.ts
Rate limiters are imported from a centralized middleware:
// backend/src/routes/auth.js
const {
authLimiter: signinLimiter,
passwordResetLimiter,
} = require('../middlewares/rateLimiter');
// Preconfigured limiters in rateLimiter.ts:
const authLimiter = createRateLimiter({
keyPrefix: 'auth',
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: 'Too many authentication attempts. Please try again later.',
});
// Self-registration is disabled; no signup limiter is registered.
const passwordResetLimiter = createRateLimiter({
keyPrefix: 'password-reset',
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
message: 'Too many password reset requests. Please try again later.',
});
Features:
- Uses centralized in-memory Map with automatic cleanup every 5 minutes
- Adds standard rate limit headers (
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset) - Returns 429 status with JSON response when exceeded
- Skips rate limiting in development for localhost
Token Management
User Model Fields
| Field | Type | Description |
|---|---|---|
emailVerified |
BOOLEAN | Whether email has been verified |
emailVerificationToken |
TEXT | Token for email verification (40-char hex) |
emailVerificationTokenExpiresAt |
DATE | Token expiration timestamp |
passwordResetToken |
TEXT | Token for password reset (40-char hex) |
passwordResetTokenExpiresAt |
DATE | Token expiration timestamp |
Token Lifecycle
┌─────────────────────────────────────────────────────────────────────────────┐
│ Token Lifecycle │
│ │
│ 1. Token Generated │
│ └── crypto.randomBytes(20).toString('hex') │
│ └── Expiration: Date.now() + 24 hours │
│ │
│ 2. Token Stored │
│ └── users.emailVerificationToken / passwordResetToken │
│ └── users.emailVerificationTokenExpiresAt / passwordResetTokenExpiresAt │
│ │
│ 3. Email Sent │
│ └── Link: {host}/verify-email?token={token} │
│ └── Link: {host}/password-reset?token={token} │
│ │
│ 4. Token Validated │
│ └── Check token exists │
│ └── Check tokenExpiresAt > Date.now() │
│ │
│ 5. Token Consumed │
│ └── emailVerified = true (verification) │
│ └── password updated (reset) │
└─────────────────────────────────────────────────────────────────────────────┘
Email Verification Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ Email Verification Flow │
│ │
│ User Backend AWS SES │
│ │ │ │ │
│ │ POST /api/auth/send-email-address-verification-email │ │
│ │───────────────────────────>│ │ │
│ │ │ │ │
│ │ │ Create user │ │
│ │ │ Generate verification token │ │
│ │ │ │ │
│ │ │ EmailSender.send() │ │
│ │ │──────────────────────────────>│ │
│ │ │ │ │
│ │ │<──────────────────────────────│ │
│ │<───────────────────────────│ 200 OK (JWT token) │ │
│ │ │ │ │
│ │ [Email arrives with verification link] │
│ │ │ │ │
│ │ PUT /api/auth/verify-email │ │ │
│ │ { token: "..." } │ │ │
│ │───────────────────────────>│ │ │
│ │ │ │ │
│ │ │ Validate token & expiry │ │
│ │ │ Set emailVerified = true │ │
│ │ │ │ │
│ │<───────────────────────────│ 200 OK │ │
│ │ │ │ │
│ │ [User can now sign in normally] │
└─────────────────────────────────────────────────────────────────────────────┘
Invitation Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ Invitation Flow │
│ │
│ Admin Backend New User │
│ │ │ │ │
│ │ POST /api/users │ │ │
│ │ { email, sendInvite:true } │ │ │
│ │───────────────────────────>│ │ │
│ │ │ │ │
│ │ │ Create user record │ │
│ │ │ Generate password reset token │ │
│ │ │ Send invitation email ───────>│ │
│ │ │ │ │
│ │<───────────────────────────│ 200 OK │ │
│ │ │ │ │
│ │ │ [Email with invitation link] │
│ │ │ │ │
│ │ │ Click link: /password-reset?token= │
│ │ │<──────────────────────────────│ │
│ │ │ │ │
│ │ │ PUT /api/auth/password-reset │ │
│ │ │ { token, password } │ │
│ │ │ │ │
│ │ │ Set password │ │
│ │ │ (Auto-verifies email) │ │
│ │ │ │ │
│ │ │──────────────────────────────>│ │
│ │ │ 200 OK - Account ready │ │
└─────────────────────────────────────────────────────────────────────────────┘
Error Handling
Validation Errors
| Error Key | Message | HTTP Status |
|---|---|---|
auth.userNotVerified |
"Sorry, your email has not been verified yet" | 400 |
auth.emailAddressVerificationEmail.invalidToken |
"Email verification link is invalid or has expired" | 400 |
auth.emailAddressVerificationEmail.error |
"Email not recognized" | 400 |
auth.passwordReset.invalidToken |
"Password reset link is invalid or has expired" | 400 |
auth.passwordReset.error |
"Email not recognized" | 400 |
Graceful Degradation
When email is not configured (EmailSender.isConfigured === false):
- Signup: Users are automatically marked as verified
- Signin: Email verification check is bypassed
- Password Reset: Endpoint returns success but no email is sent
// backend/src/services/auth.ts
if (!EmailSender.isConfigured) {
user.emailVerified = true; // Auto-verify when email not configured
}
File Structure
backend/src/
├── middlewares/
│ └── rateLimiter.ts # Centralized rate limiting middleware
└── services/
├── email/
│ ├── index.ts # EmailSender class
│ ├── list/
│ │ ├── addressVerification.ts # Email verification template
│ │ ├── invitation.ts # User invitation template
│ │ └── passwordReset.ts # Password reset template
│ └── htmlTemplates/
│ ├── addressVerification/
│ │ └── emailAddressVerification.html
│ ├── invitation/
│ │ └── invitationTemplate.html
│ └── passwordReset/
│ └── passwordResetEmail.html
└── notifications/
├── helpers.js # getNotification() function
├── list.js # Message catalog
└── errors/
├── forbidden.js # ForbiddenError class
└── validation.js # ValidationError class
Known Considerations
-
AWS SES Region: The system is configured for
us-east-1. For other regions, updateconfig.email.host. -
Token Expiration: All tokens expire after 24 hours. This was increased from the original 6 minutes to accommodate email delivery delays.
-
Rate Limiting: Uses centralized in-memory rate limiting in
backend/src/middlewares/rateLimiter.ts(not distributed). Rate limits reset on server restart. Automatic cleanup of expired entries every 5 minutes. -
SES Configuration Set: The
X-SES-CONFIGURATION-SET: flatlogic-appheader enables SES tracking features. -
Email Not Configured Mode: The system gracefully handles missing email configuration by auto-verifying users, useful for development environments.
-
Invitation vs Password Reset: Invitations use the same token mechanism as password reset, with a different email template. The link includes
&invitation=trueto indicate the context. -
TLS Certificate Validation: Can be disabled via
EMAIL_TLS_REJECT_UNAUTHORIZED=falsefor development environments with self-signed certificates.