Skip to main content

Mail System

The mail system handles all transactional email delivery for the platform, supporting SMTP, Resend, and a development faker provider. Emails are rendered using Handlebars templates and triggered by domain events via NestJS @OnEvent decorators.

Architecture

apps/api/src/mail/
mail.service.ts # Event listeners and email orchestration
mail.module.ts # Module config (SMTP, Resend, templates)
types.ts # SendMailOptions interface
providers/
mailer.service.ts # Multi-provider dispatch (SMTP, Resend, faker)
faker-mailer.service.ts # Development-only logger provider

apps/api/src/templates/
signup-confirmation.hbs # Account signup verification
forgot-password.hbs # Password reset link
password-changed.hbs # Password change confirmation
welcome.hbs # Post-confirmation welcome email
new-device-login.hbs # New device login alert
account-deletion.hbs # Account deletion confirmation
member-invitation.hbs # Work collaboration invitation

Mail Providers

The MailerService selects a provider based on config.mail.provider():

ProviderValueDescription
SMTPsmtpStandard SMTP transport via @nestjs-modules/mailer
ResendresendResend API client, falls back to faker if API key is missing
Faker(default)Logs email data to console; used in development

SMTP Configuration

Config MethodEnvironment VariableDescription
config.mail.smtpHost()MAIL_SMTP_HOSTSMTP server hostname
config.mail.smtpPort()MAIL_SMTP_PORTSMTP server port
config.mail.smtpSecure()MAIL_SMTP_SECUREUse TLS
config.mail.smtpIgnoreTLS()MAIL_SMTP_IGNORE_TLSIgnore TLS errors
config.mail.smtpUser()MAIL_SMTP_USERSMTP username
config.mail.smtpPassword()MAIL_SMTP_PASSWORDSMTP password
config.mail.from()MAIL_FROMDefault "From" address
config.mail.provider()MAIL_PROVIDERsmtp, resend, or omit

Resend Configuration

Config MethodEnvironment VariableDescription
config.mail.resend.apiKey()RESEND_APIKEYResend API key
config.mail.resend.emailFrom()RESEND_FROMResend sender address

Email Templates

Templates use Handlebars (.hbs) with inline CSS enabled. All templates receive common branding context:

{
appName: string, // Application name from config
companyOwner: string, // Company owner name
platformWebsite: string, // Platform URL
currentYear: number // Current year for footer
}

Available Templates

TemplateEventAdditional Context
signup-confirmationUserCreatedEventfirstName, confirmationUrl, confirmationToken
forgot-passwordUserForgotPasswordEventfirstName, resetUrl, resetToken, expiresIn
password-changedUserPasswordChangedEventfirstName, changedAt, ipAddress, location, device, browser, secureAccountUrl
welcomeUserConfirmedEventfirstName, dashboardUrl
new-device-loginUserNewDeviceLoginEventfirstName, loginTime, device, browser, location, ipAddress, verifyUrl, verifyToken, secureAccountUrl
account-deletionUserAccountDeletionEventfirstName, deleteUrl, deleteToken, keepAccountUrl, expiresIn
member-invitationMemberInvitedEventinviteeName, inviterName, workName, roleName, workUrl

Event-Driven Delivery

The MailService subscribes to domain events using @OnEvent():

@OnEvent(UserCreatedEvent.EVENT_NAME)
async sendSignupConfirmation(data: UserCreatedEvent): Promise<void> {
await this.mailerService.sendMail({
to: data.user.email,
subject: `Confirm your ${appName} account`,
template: 'signup-confirmation',
context: { ...brandingContext, firstName: data.user.username, ... },
});
}

All email sends are wrapped in try/catch blocks and errors are logged without throwing, ensuring email failures do not break the calling flow.

Adding a New Template

  1. Create a .hbs file in apps/api/src/templates/:

    <h1>Hello {{firstName}},</h1>
    <p>Your notification from {{appName}}.</p>
    <p>&copy; {{currentYear}} {{companyOwner}}</p>
  2. Define a new event class in apps/api/src/events/.

  3. Add an @OnEvent() handler in MailService:

    @OnEvent(MyNewEvent.EVENT_NAME)
    async sendMyNewEmail(data: MyNewEvent): Promise<void> {
    await this.mailerService.sendMail({
    to: data.user.email,
    subject: 'My Subject',
    template: 'my-new-template',
    context: { ...this.getBrandingContext(), ...data },
    });
    }
  4. Emit the event from your service:

    this.eventEmitter.emit(MyNewEvent.EVENT_NAME, new MyNewEvent({ ... }));

SendMailOptions

The types.ts file defines the SendMailOptions interface:

interface SendMailOptions {
to?: string | Address | Array<string | Address>;
cc?: string | Address | Array<string | Address>;
bcc?: string | Address | Array<string | Address>;
from?: string | Address;
subject?: string;
text?: string | Buffer;
html?: string | Buffer;
template?: string; // Handlebars template name (without .hbs)
context?: Record<string, any>; // Template variables
}

Module Registration

@Module({
imports: [
MailerModule.forRootAsync({
useFactory: () => ({
transport: { host, port, secure, auth: { user, pass } },
defaults: { from: config.mail.from() },
template: {
dir: path.join(process.cwd(), 'src/templates'),
adapter: new HandlebarsAdapter(undefined, { inlineCssEnabled: true })
}
})
})
],
providers: [
{ provide: 'RESEND_CLIENT', useFactory: () => new Resend(apiKey) },
MailService,
MailerService,
FakerMailerService
]
})
export class MailModule {}