Email Templates Guide β
Learn how to create and use internationalized email templates in Flowfull-Node.
Overview β
Flowfull-Node includes a powerful email system with:
- i18n Support - Multi-language templates (English, Spanish, etc.)
- Variable Replacement - Dynamic content with
syntax - Conditional Rendering - Show/hide content based on conditions
- ZeptoMail Integration - Reliable email delivery (recommended provider)
- Template Service - Automatic language detection and fallback
- Email Service - Retry logic and error handling
Recommended Email Provider
ZeptoMail is the recommended email provider for Flowfull-Node. It offers reliable delivery, excellent deliverability rates, and a generous free tier perfect for getting started.
Quick Start β
1. Configure Environment β
# ZeptoMail Configuration
ZEPTOMAIL_API_KEY=Zoho-enczapikey your_api_key_here
# Email Settings
EMAIL_FROM_ADDRESS=noreply@yourdomain.com
EMAIL_FROM_NAME=Your App Name
EMAIL_REPLY_TO_ADDRESS=support@yourdomain.com
EMAIL_REPLY_TO_NAME=Support Team
# Organization Info (used in templates)
ORGANIZATION_NAME=Your Organization
ORGANIZATION_EMAIL=info@yourdomain.com
ORGANIZATION_WEBSITE=https://yourdomain.com2. Send Your First Email β
import { emailService } from '@/lib/email/email-service';
import { templateService } from '@/lib/email/template-service';
// Get template with variables
const { html, subject } = templateService.getTemplate('welcome', {
user_name: 'John Doe',
user_email: 'john@example.com',
verification_link: 'https://yourdomain.com/verify?token=xxx'
}, 'en');
// Send email
const result = await emailService.sendEmail({
to: [{ address: 'john@example.com', name: 'John Doe' }],
subject,
htmlBody: html
});
if (result.success) {
console.log('β
Email sent successfully');
} else {
console.error('β Email failed:', result.message);
}Project Structure β
src/lib/email/
βββ email-service.ts # Email sending logic
βββ template-service.ts # Template loading and rendering
βββ subjects/ # Email subjects by language
β βββ en.json # English subjects
β βββ es.json # Spanish subjects
βββ templates/ # HTML email templates
βββ en/ # English templates
β βββ welcome.html
β βββ email_verification.html
β βββ password_reset.html
β βββ invitation.html
βββ es/ # Spanish templates
βββ welcome.html
βββ email_verification.html
βββ password_reset.html
βββ invitation.htmlCreating Email Templates β
Template Structure β
Email templates use HTML with variable placeholders:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{subject}}</title>
<style>
/* Your styles here */
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; }
.button { background: #006AFF; color: white; padding: 12px 24px; }
</style>
</head>
<body>
<div class="container">
<h1>Hello {{user_name}}!</h1>
<p>Welcome to {{organization_name}}.</p>
<a href="{{verification_link}}" class="button">
Verify Email
</a>
<p>Β© {{current_year}} {{organization_name}}</p>
</div>
</body>
</html>Variable Syntax β
Use double curly braces for variables:
{{variable_name}}Available Default Variables β
These variables are automatically available in all templates:
| Variable | Description | Example |
|---|---|---|
| Current year | 2025 |
| Current date (localized) | 12/09/2025 |
| Current time (localized) | 3:45 PM |
| From env ORGANIZATION_NAME | Your Company |
| From env ORGANIZATION_EMAIL | info@company.com |
| From env ORGANIZATION_WEBSITE | https://company.com |
Custom Variables β
Pass any custom variables when getting the template:
const { html, subject } = templateService.getTemplate('welcome', {
user_name: 'Jane Smith',
user_email: 'jane@example.com',
account_type: 'Premium',
trial_days: 30,
features: ['Feature 1', 'Feature 2', 'Feature 3']
});Conditional Rendering β
Simple Conditionals β
Show content only if a variable exists:
<!-- Show only if user_name exists -->
{{#if user_name}}
<p>Hello {{user_name}}!</p>
{{/if}}
<!-- Show if variable doesn't exist -->
{{#unless user_name}}
<p>Hello there!</p>
{{/unless}}Comparison Conditionals β
<!-- Check equality -->
{{#if account_type === 'Premium'}}
<div class="premium-badge">Premium Member</div>
{{/if}}
<!-- Check inequality -->
{{#if status !== 'active'}}
<div class="warning">Please activate your account</div>
{{/if}}i18n (Internationalization) β
Language Detection β
The template service automatically detects language from:
- Explicit parameter -
getTemplate('welcome', vars, 'es') - Environment variable -
DEFAULT_LANGUAGE=es - Fallback - English (
en)
Creating Multi-Language Templates β
1. Create Template Files β
templates/
βββ en/
β βββ welcome.html
βββ es/
βββ welcome.html2. Create Subject Files β
// subjects/en.json
{
"welcome": "Welcome to {{organization_name}}!",
"email_verification": "Verify your email address",
"password_reset": "Reset your password"
}// subjects/es.json
{
"welcome": "Β‘Bienvenido a {{organization_name}}!",
"email_verification": "Verifica tu direcciΓ³n de correo",
"password_reset": "Restablece tu contraseΓ±a"
}3. Use in Code β
// English
const { html, subject } = templateService.getTemplate('welcome', vars, 'en');
// Spanish
const { html, subject } = templateService.getTemplate('welcome', vars, 'es');
// Auto-detect from env
const { html, subject } = templateService.getTemplate('welcome', vars);Language Fallback β
If a template doesn't exist in the requested language, it automatically falls back to the default language:
// Request Spanish template
const result = templateService.getTemplate('new_feature', vars, 'es');
// If es/new_feature.html doesn't exist, uses en/new_feature.html
// Console: "Template new_feature not found in es, falling back to en"Email Service API β
Basic Send β
import { emailService } from '@/lib/email/email-service';
const result = await emailService.sendEmail({
to: [
{ address: 'user@example.com', name: 'User Name' }
],
subject: 'Welcome!',
htmlBody: '<h1>Hello!</h1>'
});Send with Retry β
Automatically retries failed emails with exponential backoff:
const result = await emailService.sendEmailWithRetry({
to: [{ address: 'user@example.com', name: 'User' }],
subject: 'Important Email',
htmlBody: html
}, 3); // Max 3 retries
// Retry delays: 2s, 4s, 8sCustom From/Reply-To β
await emailService.sendEmail({
from: {
address: 'custom@yourdomain.com',
name: 'Custom Sender'
},
replyTo: {
address: 'support@yourdomain.com',
name: 'Support Team'
},
to: [{ address: 'user@example.com', name: 'User' }],
subject: 'Custom Email',
htmlBody: html
});Multiple Recipients β
await emailService.sendEmail({
to: [
{ address: 'user1@example.com', name: 'User 1' },
{ address: 'user2@example.com', name: 'User 2' },
{ address: 'user3@example.com', name: 'User 3' }
],
subject: 'Bulk Email',
htmlBody: html
});Text + HTML Body β
await emailService.sendEmail({
to: [{ address: 'user@example.com', name: 'User' }],
subject: 'Email with Text Fallback',
htmlBody: '<h1>Hello!</h1><p>This is HTML</p>',
textBody: 'Hello! This is plain text'
});Template Service API β
Get Template β
import { templateService } from '@/lib/email/template-service';
const result = templateService.getTemplate(
'template_name', // Template name (without .html)
{ // Variables
user_name: 'John',
custom_var: 'value'
},
'en' // Language (optional)
);
if (result) {
const { html, subject } = result;
// Use html and subject
} else {
console.error('Template not found');
}Check Template Exists β
if (templateService.templateExists('welcome', 'es')) {
console.log('Spanish welcome template exists');
}Get Available Languages β
const languages = templateService.getAvailableLanguages();
// ['en', 'es']Get Current Language β
const currentLang = templateService.getLanguage();
// 'en' or 'es'Common Email Templates β
Welcome Email β
const { html, subject } = templateService.getTemplate('welcome', {
user_name: user.name,
user_email: user.email,
login_link: `${process.env.FRONTEND_URL}/login`
});
await emailService.sendEmail({
to: [{ address: user.email, name: user.name }],
subject,
htmlBody: html
});Email Verification β
import { generateTrustToken } from '@/lib/utils/trust-tokens';
// Generate verification token
const token = await generateTrustToken({
type: 'email_verification',
userId: user.id,
email: user.email
}, 24); // 24 hours
const { html, subject } = templateService.getTemplate('email_verification', {
user_name: user.name,
verification_link: `${process.env.FRONTEND_URL}/verify?token=${token}`
});
await emailService.sendEmail({
to: [{ address: user.email, name: user.name }],
subject,
htmlBody: html
});Password Reset β
import { generateTrustToken } from '@/lib/utils/trust-tokens';
// Generate reset token (1 hour expiration)
const token = await generateTrustToken({
type: 'password_reset',
userId: user.id,
email: user.email
}, 1);
const { html, subject } = templateService.getTemplate('password_reset', {
user_name: user.name,
reset_link: `${process.env.FRONTEND_URL}/reset-password?token=${token}`,
ip_address: request.ip,
user_agent: request.headers['user-agent']
});
await emailService.sendEmailWithRetry({
to: [{ address: user.email, name: user.name }],
subject,
htmlBody: html
}, 3);Invitation Email β
const token = await generateTrustToken({
type: 'invitation',
userId: inviter.id,
memberId: newMember.id,
resourceId: organization.id,
role: 'member',
invitedBy: inviter.id,
metadata: {
organizationName: organization.name
}
}, 168); // 7 days
const { html, subject } = templateService.getTemplate('invitation', {
inviter_name: inviter.name,
organization_name: organization.name,
role: 'Member',
invitation_link: `${process.env.FRONTEND_URL}/accept-invite?token=${token}`
});
await emailService.sendEmail({
to: [{ address: newMember.email, name: newMember.name }],
subject,
htmlBody: html
});Best Practices β
1. Always Use Templates β
β Don't hardcode HTML in your code:
// Bad
const html = `<h1>Hello ${user.name}</h1>`;β Do use template service:
// Good
const { html } = templateService.getTemplate('welcome', { user_name: user.name });2. Use Retry for Important Emails β
// Critical emails (password reset, verification)
await emailService.sendEmailWithRetry(options, 3);
// Non-critical emails (newsletters, notifications)
await emailService.sendEmail(options);3. Validate Email Addresses β
import { z } from 'zod';
const emailSchema = z.string().email();
try {
emailSchema.parse(userEmail);
// Email is valid
} catch {
return { error: 'Invalid email address' };
}4. Log Email Events β
const result = await emailService.sendEmail(options);
if (result.success) {
console.log(`[EMAIL] β
Sent ${templateName} to ${user.email}`);
// Optional: Log to database
await db.insertInto('email_logs').values({
user_id: user.id,
template: templateName,
recipient: user.email,
status: 'sent',
sent_at: new Date()
}).execute();
} else {
console.error(`[EMAIL] β Failed to send ${templateName}:`, result.message);
}5. Test Templates Locally β
Create a test script to preview templates:
// scripts/test-email-template.ts
import { templateService } from '../src/lib/email/template-service';
import { writeFileSync } from 'fs';
const { html } = templateService.getTemplate('welcome', {
user_name: 'Test User',
user_email: 'test@example.com',
verification_link: 'https://example.com/verify?token=test'
});
writeFileSync('preview.html', html);
console.log('β
Template saved to preview.html');bun run scripts/test-email-template.ts
# Open preview.html in browser6. Use Environment Variables β
Never hardcode URLs or organization info:
// β Bad
const link = 'https://myapp.com/verify';
// β
Good
const link = `${process.env.FRONTEND_URL}/verify`;7. Handle Errors Gracefully β
try {
const result = await emailService.sendEmail(options);
if (!result.success) {
// Log error but don't block user flow
console.error('[EMAIL] Send failed:', result.message);
// Optional: Queue for retry later
await queueEmailForRetry(options);
}
} catch (error) {
console.error('[EMAIL] Unexpected error:', error);
// Don't throw - email failure shouldn't break the app
}Troubleshooting β
Email Not Sending β
Check ZeptoMail API Key
bashecho $ZEPTOMAIL_API_KEY # Should start with "Zoho-enczapikey"Verify Environment Variables
typescriptconsole.log('From:', process.env.EMAIL_FROM_ADDRESS); console.log('Name:', process.env.EMAIL_FROM_NAME);Check ZeptoMail Dashboard
- Login to ZeptoMail
- Check "Email Logs" for delivery status
- Look for bounce/spam reports
Template Not Found β
Check File Exists
bashls src/lib/email/templates/en/ # Should show your_template.htmlCheck Template Name
typescript// File: welcome.html // Use: 'welcome' (without .html) templateService.getTemplate('welcome', vars);Check Language
typescript// If requesting 'es' but only 'en' exists // Will fallback to 'en' automatically
Variables Not Replacing β
Check Variable Syntax
html<!-- β Correct --> {{user_name}} <!-- β Wrong --> {user_name} ${user_name}Check Variable Name
typescript// Template uses: {{user_name}} // Must pass: { user_name: 'John' } // Not: { userName: 'John' }Check Variable Value
typescript// undefined/null won't replace { user_name: undefined } // Shows {{user_name}} { user_name: 'John' } // Shows John
Resources β
- ZeptoMail Docs: zeptomail.zoho.com/help
- HTML Email Guide: emailonacid.com/blog
- Email Testing: litmus.com
Next Steps β
- Trust Tokens Guide - Learn about secure token generation
- Protected Routes - Implement authentication
- Deployment Guide - Deploy to production