Skip to main content

Built-in HTTP Mailer

node-auth ships a built-in HTTP mailer transport (MailerService) that sends transactional emails by posting JSON payloads to any HTTP email relay โ€” Mailgun, SendGrid, Brevo, Postmark, or a custom gateway.

No SMTP setup needed. Point it at your relay's endpoint, set an API key, and every auth email (password reset, magic link, welcome, email verification, email-changed notification) is sent automatically using built-in bilingual (๐Ÿ‡ฌ๐Ÿ‡ง English / ๐Ÿ‡ฎ๐Ÿ‡น Italian) templates.


How it worksโ€‹


Configurationโ€‹

import { AuthConfigurator } from 'awesome-node-auth';

const auth = new AuthConfigurator({
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,

email: {
// Single URL (classic, works as before)
siteUrl: 'https://yourapp.com',

// โ€” OR โ€” array of allowed front-end origins (dynamic mode).
// For email links the FIRST entry is used as the base URL.
// For OAuth redirects the URL is resolved dynamically from the request
// Origin/Referer header (see CORS & Dynamic siteUrl guide).
// siteUrl: ['https://app.example.com', 'https://admin.example.com'],

mailer: {
endpoint: process.env.MAILER_ENDPOINT!, // e.g. https://api.brevo.com/v3/smtp/email
apiKey: process.env.MAILER_API_KEY!,
from: 'no-reply@yourapp.com',
fromName: 'Your App', // optional sender display name
provider: 'brevo', // optional โ€” forwarded to the relay as-is
defaultLang: 'en', // 'en' (default) | 'it'
},
},
}, userStore);

MailerConfig referenceโ€‹

FieldTypeRequiredDescription
endpointstringโœ…Full URL of the HTTP relay (POST target)
apiKeystringโœ…Sent as X-API-Key header
fromstringโœ…Sender email address
fromNamestringโŒSender display name
providerstringโŒForwarded to relay as provider field
defaultLang'en' | 'it'โŒDefault template language (default: 'en')

Request body sent to the relayโ€‹

{
"to": "user@example.com",
"subject": "Reset your password",
"html": "<p>Click <a href=\"โ€ฆ\">here</a> to resetโ€ฆ</p>",
"text": "Click here to reset: https://โ€ฆ",
"from": "no-reply@yourapp.com",
"fromName": "Your App",
"provider": "brevo"
}

The relay receives a standard POST with Content-Type: application/json and X-API-Key header. Adapt to your provider's format in the relay if needed.


Per-request language overrideโ€‹

Users can request a specific email language by including emailLang in the request body:

curl -X POST /auth/forgot-password \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","emailLang":"it"}'

Supported values: 'en', 'it'.


Built-in email templatesโ€‹

TriggerMethodTemplate (en)Template (it)
POST /auth/forgot-passwordsendPasswordReset"Reset your password""Reimposta la tua password"
POST /auth/magic-link/sendsendMagicLink"Your magic sign-in link""Il tuo link di accesso"
POST /auth/registersendWelcome"Welcome! Your account has been created""Benvenuto! Il tuo account รจ stato creato"
POST /auth/send-verification-emailsendVerificationEmail"Verify your email address""Verifica il tuo indirizzo email"
POST /auth/change-email/confirmsendEmailChanged"Your email address has been updated""Il tuo indirizzo email รจ stato aggiornato"

Custom callback overridesโ€‹

If you prefer to send emails yourself (e.g. with a custom template engine), use the callback overrides in AuthConfig.email. Callbacks always take priority over the mailer transport:

const auth = new AuthConfigurator({
email: {
siteUrl: 'https://yourapp.com',

// Override: custom password-reset email
sendPasswordReset: async (to, token, link, lang) => {
await myMailer.send({
to,
subject: 'Reset your password',
html: myTemplateEngine.render('password-reset', { link, lang }),
});
},

// Override: custom magic-link email
sendMagicLink: async (to, token, link, lang) => {
await myMailer.send({ to, subject: 'Your login link', html: `<a href="${link}">Sign in</a>` });
},

// Override: welcome email (e.g. after admin creates user via admin panel)
sendWelcome: async (to, data, lang) => {
await myMailer.send({ to, subject: 'Welcome', html: myTemplateEngine.render('welcome', data) });
},

// Override: email verification
sendVerificationEmail: async (to, token, link, lang) => {
await myMailer.send({ to, subject: 'Verify your email', html: `<a href="${link}">Verify</a>` });
},

// Override: email-changed notification
sendEmailChanged: async (to, newEmail, lang) => {
await myMailer.send({ to, subject: 'Email updated', html: `Your email is now ${newEmail}` });
},
},
}, userStore);

Using MailerService standaloneโ€‹

The MailerService class is exported and can be used directly in your own services:

import { MailerService } from 'awesome-node-auth';

const mailer = new MailerService({
endpoint: 'https://api.brevo.com/v3/smtp/email',
apiKey: process.env.MAILER_API_KEY!,
from: 'no-reply@yourapp.com',
defaultLang: 'it',
});

// Send individual emails
await mailer.sendPasswordReset('user@example.com', token, 'https://app.com/reset?token=โ€ฆ');
await mailer.sendMagicLink('user@example.com', token, 'https://app.com/verify?token=โ€ฆ');
await mailer.sendWelcome('user@example.com', { loginUrl: 'https://app.com/login' });
await mailer.sendVerificationEmail('user@example.com', token, 'https://app.com/verify-email?token=โ€ฆ');
await mailer.sendEmailChanged('old@example.com', 'new@example.com');

The siteUrl field accepts either a single string or an array of strings. When you have multiple front-end origins (e.g. a public app and an admin panel) pass an array โ€” the first entry is used as the base URL for all email links.

email: {
// First entry is used for all email links (magic link, password reset, etc.)
siteUrl: ["https://app.example.com", "https://admin.example.com"],
mailer: { /* โ€ฆ */ },
}

For OAuth post-login redirects the URL is resolved dynamically from the Origin / Referer request header and validated against the same array. See CORS & Dynamic siteUrl for the complete guide.


Dynamic templates with ITemplateStoreโ€‹

Instead of using the hardcoded bilingual templates, you can override any or all of them at runtime by passing an ITemplateStore to AuthConfigurator.

When a templateStore is provided and a template exists in the store for the requested templateId, it is used in place of the built-in fallback. If no custom template is found, the built-in template is used transparently.

Setupโ€‹

import { AuthConfigurator, MemoryTemplateStore } from 'awesome-node-auth';
import type { ITemplateStore } from 'awesome-node-auth';

// Use the built-in in-memory store (good for development / testing)
const templateStore: ITemplateStore = new MemoryTemplateStore();

const auth = new AuthConfigurator(
{
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!,
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET!,
email: {
siteUrl: 'https://yourapp.com',
mailer: { endpoint: process.env.MAILER_ENDPOINT!, apiKey: process.env.MAILER_API_KEY!, from: 'noreply@yourapp.com' },
},
templateStore, // โ† inject here
},
userStore,
);

// Pre-populate a template at startup (or load from DB in production)
await templateStore.updateMailTemplate('magic-link', {
baseHtml: `<p>{{T.greeting}}</p><p><a href="{{URL}}">{{T.cta}}</a></p>`,
baseText: `{{T.greeting}}\n{{T.cta}}: {{URL}}`,
translations: {
en: { subject: 'Your magic sign-in link', greeting: 'Hello!', cta: 'Click here to sign in' },
it: { subject: 'Il tuo link di accesso', greeting: 'Ciao!', cta: 'Clicca qui per accedere' },
},
});

Pass the same templateStore instance to createAdminRouter to manage templates through the Admin UI:

app.use('/admin', createAdminRouter(userStore, {
accessPolicy: 'first-user',
jwtSecret: process.env.ACCESS_TOKEN_SECRET!,
templateStore, // โ† enables the ๐Ÿ“ง Email & UI tab
}));

Template interpolation syntaxโ€‹

Templates support two interpolation families:

SyntaxResolves toExample
{{T.key}}Translation string for the current language{{T.subject}} โ†’ "Reset your password"
{{KEY}}Data variable injected by the sending method{{URL}} โ†’ "https://app.com/reset?token=โ€ฆ"

Available data variables per template ID:

Template IDData variables
password-reset{{URL}} (reset link)
magic-link{{URL}} (magic link)
welcome{{loginUrl}} (login page URL)
verify-email{{URL}} (verification link)
email-changed{{newEmail}} (new email address)

Missing variables or translation keys are rendered as [key] so you can spot gaps quickly during development.


Implementing a production ITemplateStoreโ€‹

For production, implement ITemplateStore against your database (the interface is the same regardless of DB):

import type { ITemplateStore, MailTemplate, UiTranslation } from 'awesome-node-auth';

export class MongoTemplateStore implements ITemplateStore {
constructor(private readonly db: Db) {}

async getMailTemplate(id: string): Promise<MailTemplate | null> {
return this.db.collection<MailTemplate>('mail_templates').findOne({ id }) ?? null;
}
async listMailTemplates(): Promise<MailTemplate[]> {
return this.db.collection<MailTemplate>('mail_templates').find().toArray();
}
async updateMailTemplate(id: string, tpl: Partial<MailTemplate>): Promise<void> {
await this.db.collection('mail_templates').updateOne(
{ id },
{ $set: { ...tpl, id } },
{ upsert: true },
);
}
async getUiTranslations(page: string): Promise<UiTranslation | null> {
return this.db.collection<UiTranslation>('ui_translations').findOne({ page }) ?? null;
}
async listUiTranslations(): Promise<UiTranslation[]> {
return this.db.collection<UiTranslation>('ui_translations').find().toArray();
}
async updateUiTranslations(page: string, translations: Record<string, Record<string, string>>): Promise<void> {
await this.db.collection('ui_translations').updateOne(
{ page },
{ $set: { page, translations } },
{ upsert: true },
);
}
}

See ITemplateStore API Reference for the complete interface definition.