Skip to content

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 ​

env
# 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.com

2. Send Your First Email ​

typescript
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.html

Creating Email Templates ​

Template Structure ​

Email templates use HTML with variable placeholders:

html
<!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:

html
{{variable_name}}

Available Default Variables ​

These variables are automatically available in all templates:

VariableDescriptionExample
Current year2025
Current date (localized)12/09/2025
Current time (localized)3:45 PM
From env ORGANIZATION_NAMEYour Company
From env ORGANIZATION_EMAILinfo@company.com
From env ORGANIZATION_WEBSITEhttps://company.com

Custom Variables ​

Pass any custom variables when getting the template:

typescript
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:

html
<!-- 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 ​

html
<!-- 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:

  1. Explicit parameter - getTemplate('welcome', vars, 'es')
  2. Environment variable - DEFAULT_LANGUAGE=es
  3. Fallback - English (en)

Creating Multi-Language Templates ​

1. Create Template Files ​

templates/
β”œβ”€β”€ en/
β”‚   └── welcome.html
└── es/
    └── welcome.html

2. Create Subject Files ​

json
// subjects/en.json
{
  "welcome": "Welcome to {{organization_name}}!",
  "email_verification": "Verify your email address",
  "password_reset": "Reset your password"
}
json
// 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 ​

typescript
// 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:

typescript
// 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 ​

typescript
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:

typescript
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, 8s

Custom From/Reply-To ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
if (templateService.templateExists('welcome', 'es')) {
  console.log('Spanish welcome template exists');
}

Get Available Languages ​

typescript
const languages = templateService.getAvailableLanguages();
// ['en', 'es']

Get Current Language ​

typescript
const currentLang = templateService.getLanguage();
// 'en' or 'es'

Common Email Templates ​

Welcome Email ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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:

typescript
// Bad
const html = `<h1>Hello ${user.name}</h1>`;

βœ… Do use template service:

typescript
// Good
const { html } = templateService.getTemplate('welcome', { user_name: user.name });

2. Use Retry for Important Emails ​

typescript
// Critical emails (password reset, verification)
await emailService.sendEmailWithRetry(options, 3);

// Non-critical emails (newsletters, notifications)
await emailService.sendEmail(options);

3. Validate Email Addresses ​

typescript
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 ​

typescript
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:

typescript
// 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');
bash
bun run scripts/test-email-template.ts
# Open preview.html in browser

6. Use Environment Variables ​

Never hardcode URLs or organization info:

typescript
// ❌ Bad
const link = 'https://myapp.com/verify';

// βœ… Good
const link = `${process.env.FRONTEND_URL}/verify`;

7. Handle Errors Gracefully ​

typescript
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 ​

  1. Check ZeptoMail API Key

    bash
    echo $ZEPTOMAIL_API_KEY
    # Should start with "Zoho-enczapikey"
  2. Verify Environment Variables

    typescript
    console.log('From:', process.env.EMAIL_FROM_ADDRESS);
    console.log('Name:', process.env.EMAIL_FROM_NAME);
  3. Check ZeptoMail Dashboard

    • Login to ZeptoMail
    • Check "Email Logs" for delivery status
    • Look for bounce/spam reports

Template Not Found ​

  1. Check File Exists

    bash
    ls src/lib/email/templates/en/
    # Should show your_template.html
  2. Check Template Name

    typescript
    // File: welcome.html
    // Use: 'welcome' (without .html)
    templateService.getTemplate('welcome', vars);
  3. Check Language

    typescript
    // If requesting 'es' but only 'en' exists
    // Will fallback to 'en' automatically

Variables Not Replacing ​

  1. Check Variable Syntax

    html
    <!-- βœ… Correct -->
    {{user_name}}
    
    <!-- ❌ Wrong -->
    {user_name}
    ${user_name}
  2. Check Variable Name

    typescript
    // Template uses: {{user_name}}
    // Must pass: { user_name: 'John' }
    // Not: { userName: 'John' }
  3. Check Variable Value

    typescript
    // undefined/null won't replace
    { user_name: undefined } // Shows {{user_name}}
    { user_name: 'John' }    // Shows John

Resources ​

Next Steps ​