Skip to main content

Mail & Email System

The template includes a fully-featured email system built on a provider-agnostic architecture. It supports multiple email providers out of the box, offers professional HTML email templates for all transactional events, and degrades gracefully when no provider is configured.

Architecture Overview

The mail system is organized across several files in lib/mail/:

FilePurpose
lib/mail/index.tsCore EmailService class and exported helper functions
lib/mail/factory.tsEmailProviderFactory for creating provider instances
lib/mail/resend.tsResend email provider implementation
lib/mail/novu.tsNovu notification provider implementation
lib/mail/mock.tsMock provider for development and testing
lib/mail/templates/HTML email templates for all transactional emails

Core Interfaces

EmailMessage

Every email sent through the system uses this standard message format:

export interface EmailMessage {
from: string;
to: string | string[];
subject: string;
html: string;
text?: string;
}

EmailProvider

All email providers implement this interface:

export interface EmailProvider {
sendEmail(message: EmailMessage): Promise<any>;
getName(): string;
}

EmailServiceConfig

Configuration used to initialize the service:

export interface EmailServiceConfig {
provider: string;
defaultFrom: string;
apiKeys: Record<string, string>;
domain: string;
novu?: EmailNovuConfig;
}

EmailService Class

The EmailService class at lib/mail/index.ts is the main entry point for sending emails. It initializes a provider based on configuration and provides methods for every transactional email type.

Initialization

const service = new EmailService({
provider: 'resend',
defaultFrom: 'info@ever.works',
domain: 'https://demo.ever.works',
apiKeys: {
resend: process.env.RESEND_API_KEY || '',
},
});

The constructor handles missing API keys gracefully:

  • If no API keys are configured, the service marks itself as unavailable
  • If initialization fails, a warning is logged and the service remains disabled
  • The isServiceAvailable() method can be checked before sending

Available Methods

MethodDescription
sendVerificationEmail(email, token)Send email verification link
sendPasswordResetEmail(email, token)Send password reset link
sendTwoFactorTokenEmail(email, token)Send 2FA code
sendNewsletterSubscriptionEmail(email)Welcome to newsletter
sendNewsletterUnsubscriptionEmail(email)Unsubscribe confirmation
sendPasswordChangeConfirmationEmail(email, userName?, ipAddress?, userAgent?)Password change notification
sendAccountCreatedEmail(userName, userEmail, companyName?)Account creation welcome
sendVerificationEmailWithTemplate(email, token, userName?)Verification with professional template
sendCustomEmail(message)Send any custom email

Provider Factory

The EmailProviderFactory at lib/mail/factory.ts creates the appropriate provider based on configuration:

export class EmailProviderFactory {
static createProvider(config: EmailServiceConfig): EmailProvider {
const provider = config.provider.toLowerCase();

switch (provider) {
case 'resend':
return new ResendProvider(config.apiKeys.resend, config.defaultFrom);
case 'novu':
return new NovuProvider(
config.apiKeys.novu,
config.defaultFrom,
config.novu
);
default:
return new MockEmailProvider();
}
}
}

If an API key is missing for the selected provider, the factory falls back to the mock provider with a console warning.

Email Providers

Resend Provider

The ResendProvider at lib/mail/resend.ts wraps the Resend SDK:

export class ResendProvider implements EmailProvider {
private resend: Resend;
private defaultFrom: string;

constructor(apiKey: string, defaultFrom: string) {
this.resend = new Resend(apiKey);
this.defaultFrom = defaultFrom;
}

async sendEmail(message: EmailMessage): Promise<CreateEmailResponse> {
return this.resend.emails.send({
from: message.from || this.defaultFrom,
to: message.to,
subject: message.subject,
html: message.html,
text: message.text,
});
}

getName(): string {
return 'resend';
}
}

Configuration:

EMAIL_PROVIDER=resend
RESEND_API_KEY=re_xxxxxxxxxxxxx
EMAIL_FROM=info@yourdomain.com

Novu Provider

The NovuProvider at lib/mail/novu.ts integrates with the Novu notification infrastructure:

export class NovuProvider implements EmailProvider {
private novu: Novu;
private defaultFrom: string;
private templateId: string;

constructor(apiKey: string, defaultFrom: string, config?: EmailNovuConfig) {
this.novu = new Novu({
secretKey: apiKey,
serverURL: config?.backendUrl,
});
this.defaultFrom = defaultFrom;
this.templateId = config?.templateId || 'email-default';
}

async sendEmail(message: EmailMessage) {
const email = Array.isArray(message.to) ? message.to[0] : message.to;
return this.novu.trigger({
to: { subscriberId: email, email },
workflowId: this.templateId,
payload: {
subject: message.subject,
body: message.html,
preheader: message.text,
from: message.from || this.defaultFrom,
},
});
}
}

Configuration:

EMAIL_PROVIDER=novu
NOVU_API_KEY=your_novu_api_key
NOVU_TEMPLATE_ID=email-default
NOVU_BACKEND_URL=https://api.novu.co

Mock Provider

The MockEmailProvider at lib/mail/mock.ts logs emails to the console for development:

export class MockEmailProvider implements EmailProvider {
async sendEmail(message: EmailMessage) {
console.log('Sending email:', message);
return Promise.resolve();
}
getName(): string {
return 'mock';
}
}

Graceful Degradation

The system uses the tryEmailOperation helper to ensure email failures never break core functionality:

async function tryEmailOperation<T>(
operation: (service: EmailService) => Promise<T>,
operationName: string
): Promise<T | EmailSkippedResult> {
try {
const service = await mailService();
if (!service.isServiceAvailable()) {
console.warn(`[EMAIL] ${operationName}: Skipped - email service not configured`);
return { skipped: true, reason: 'Email service not configured' };
}
return await operation(service);
} catch (error) {
if (error instanceof Error && error.message.includes('not available')) {
return { skipped: true, reason: error.message };
}
throw error;
}
}

This means registration, password reset, and other flows work even without a configured email provider.

Exported Helper Functions

The module exports convenience functions that wrap tryEmailOperation:

export const sendVerificationEmail = async (email: string, token: string) => {
return tryEmailOperation(
(service) => service.sendVerificationEmail(email, token),
'sendVerificationEmail'
);
};

export const sendPasswordResetEmail = async (email: string, token: string) => { ... };
export const sendNewsletterSubscriptionEmail = async (email: string) => { ... };
export const sendTwoFactorTokenEmail = async (email: string, token: string) => { ... };
export const sendPasswordChangeConfirmationEmail = async (...) => { ... };
export const sendAccountCreatedEmail = async (...) => { ... };

Import and use these directly from any server-side code:

import { sendVerificationEmail } from '@/lib/mail';

await sendVerificationEmail(user.email, verificationToken);

Email Templates

Professional HTML templates are located in lib/mail/templates/:

TemplateFile
Account Createdaccount-created.ts
Admin Notificationadmin-notification.ts
Email Verificationemail-verification.ts
Newsletter Regularnewsletter-regular.ts
Newsletter Welcomenewsletter-welcome.ts
Newsletter Unsubscribenewsletter-unsubscribe.ts
Password Change Confirmationpassword-change-confirmation.ts
Payment Successpayment-success.ts
Payment Failedpayment-failed.ts
Submission Decisionsubmission-decision.ts
Subscription Eventssubscription-events.ts
Subscription Expiredsubscription-expired.ts
Subscription Renewal Remindersubscription-renewal-reminder.ts

Each template is a function that accepts data and returns an object with subject, html, and text fields.

Configuration via Git-Based CMS

The mailService() factory function reads mail configuration from the Git-based CMS config:

async function mailService() {
const config = await getCachedConfig();
return new EmailService({
provider: config.mail?.provider || emailConfig.provider,
defaultFrom: config.mail?.default_from || emailConfig.defaultFrom,
domain: config.app_url || emailConfig.domain,
});
}

This allows administrators to change the email provider, sender address, or Novu template through the CMS configuration without code changes.

FileDescription
lib/mail/index.tsCore email service and exported helpers
lib/mail/factory.tsProvider factory pattern
lib/mail/resend.tsResend provider
lib/mail/novu.tsNovu provider
lib/mail/mock.tsMock provider for development
lib/mail/templates/All HTML email templates
lib/config/config-service.tsEmail configuration source