Email Notifications#
Three Layers#
| Layer | Entry | Purpose |
|---|---|---|
mailer.sendMail | @/server/shared/mailer | Send arbitrary HTML email, no template |
sendTemplate | @/server/modules/notification/email-templates.service | Render a DB template (email_templates) with variables, then send |
notification.sendXxxEmail | @/server/modules/notification/notification.service | Convenient wrappers for common emails (welcome, captcha, lifetime, etc.) |
All three eventually go through mailer.sendMail, which dispatches to AWS SES or Cloudflare Email Sending based on MAIL_PROVIDER.
1. Direct Send (no template)#
Simplest path — for ad-hoc notifications that don't need to be reused:
typescriptimport { mailer } from "@/server/shared/mailer"
await mailer.sendMail({
to: "user@example.com",
subject: "Your invoice is ready",
html: "<h1>Hi</h1><p>Your invoice #1234 is attached.</p>",
})
Signature:
typescriptmailer.sendMail({ to: string, subject: string, html: string }):
Promise<{ success: boolean; messageId?: string; error?: string }>
2. Template Send (recommended)#
Use templates when content is reused, the owner edits copy in admin, or you need variable interpolation:
typescriptimport { sendTemplate } from "@/server/modules/notification/email-templates.service"
await sendTemplate("welcome", "user@example.com", {
email: "user@example.com",
app_name: "Your App",
})
Signature:
typescriptsendTemplate(
templateId: string,
to: string,
variables: Record<string, string>,
): Promise<{ success: boolean; message: string }>
Templates use {{name}} placeholders (simple string replacement — no conditionals or loops). E.g. in template body:
html<p>Hi {{email}},</p>
<p>Welcome to {{app_name}}!</p>
3. Business Helpers#
The notification object wraps the project's common emails — call directly from business code:
typescriptimport { notification } from "@/server/modules/notification/notification.service"
await notification.sendWelcomeEmail(email)
await notification.sendCaptcha(email, code)
await notification.sendLifetimeEmail(email)
await notification.sendSubscriptionCreatedEmail(email)
await notification.sendSubscriptionRenewedEmail(email)
await notification.sendCreditEmail(email)
await notification.sendAccountDeletionEmail(email)
To add a new common email, append a method in notification.service.ts:
typescriptasync sendOrderShippedEmail(email: string, order_no: string) {
await sendTemplate("order-shipped", email, { email, order_no })
},
Built-in Templates#
| Template ID | Trigger |
|---|---|
welcome | User registration succeeded |
captcha | Verification code sent |
user-delete | Account deletion confirmation |
subscription-created | Subscription created |
subscription-renewed | Subscription renewed |
credit | Credit pack purchased |
lifetime | Lifetime deal purchased |
Admin Panel#
/owner/email-templates lets you:
- List all templates
- Edit subject / body / variables
- Create new templates
- Send Test — sends the current draft to the logged-in account via
mailer.sendMail
Unsubscribe#
- Users visit
/en/unsubscribeor/zh/unsubscribeto opt out of marketing emails (the old/unsubscriberedirects to the user's preferred language) - Preferences stored in
user_email_preferences - Per category:
marketing,product_updates,payment_receipts
Dev Mode#
When MAIL_SEND_ENABLED=false, no real email is sent — verification codes are printed to server logs. Set true in production.