Email Module (MailerSend)
Transactional email powered by MailerSend.
Overview
The email module wires a production-grade transactional email provider into the authentication flows of every generated project:
- ✅ Three flows out of the box — account confirmation, password reset, user invitation
- ✅ i18n ready — HTML + plain-text templates in English and French
- ✅ Dev-friendly default — without the module, rendered emails are logged to the console
- ✅ Fully isolated — emitted through a single
MailerSendServiceso you can swap providers later - ✅ Test mode built in — a fake API key in
.env.testkeeps E2E tests offline
What the module actually ships
Every SaaSFoundryAI-generated project already contains the scaffolding for transactional email: the EmailService, the translation layer, and the six templates (HTML + text for each of the three flows). What the module adds is the piece that puts bytes on the wire:
MailerSendService— a thin wrapper around themailersendnpm SDK, wired with your API key and sender identity- Uncomments
TODO mailer-service-active:markers inauth.service.ts,invitation.service.ts,env.service.tsandemail.service.ts— the realsendEmail()call replaces the dev-modeconsole.log - Registers the provider in
email.module.ts - Enables the E2E spec by renaming
email.service.disabled-spec.ts→email.service.spec.ts - Writes credentials into
.env(production values),.env.test(fake test key), and.github/workflows/deployment.yml(GitHub Actions secret reference)
This is why "without the module" is still useful: you can develop and sign users up locally without any third-party account — the emails just land in your server logs.
Installation
During project creation
sf new
# When prompted:
? Do you want to configure MailerSend for transactional emails?
→ Yes
? MailerSend API key:
→ ms_prod_abc123...
? Sender email:
→ noreply@myapp.com
? Sender name:
→ My SaaS AppAdding it to an existing project
sf update --add-modules email \
--mailersend-api-key $MAILERSEND_KEY \
--mailersend-sender-email noreply@myapp.com \
--mailersend-sender-name "My SaaS App"Or run sf update interactively and pick Email (MailerSend) from the module menu.
The installer will:
- Copy
mailersend.service.tstoapps/api/src/modules/email/services/ - Uncomment every
// TODO mailer-service-active:marker across the affected services - Register
MailerSendServiceas a provider inEmailModule - Rename the disabled unit test so it runs in CI
- Replace the
# MAILERSEND_*=placeholders in.env,.env.testanddeployment.ymlwith real values
Usage
The three built-in flows
The module is wired in automatically — you do not call it directly in your business code. The three auth flows trigger it through the existing services:
| Flow | Triggered when | Service | Template |
|---|---|---|---|
| Account confirmation | A user signs up (POST /api/auth/signup) | AuthService.signup() | templates/account-confirmation/* |
| Password reset | A user requests a reset (POST /api/auth/reset) | AuthService.resetPassword() | templates/password-reset/* |
| Organisation invitation | A member invites someone | InvitationService.create() | templates/invitation/* |
Each flow builds a {baseUrl}?token=... link and hands a fully rendered {html, text} pair to EmailService, which forwards it to MailerSendService.sendEmail().
Calling EmailService from your own modules
If you add your own flow (billing receipt, weekly digest, …) inject EmailService and call a method on it. The pattern follows the existing ones:
import { EmailService } from '@modules/email/services/email.service'
@Injectable()
export class BillingService {
constructor(private readonly emailService: EmailService) {}
async sendReceipt(user: User, invoice: Invoice) {
// Add a sendReceiptEmail method to EmailService following the existing ones,
// then call it here. Keep translations in locales/{en,fr}.ts.
await this.emailService.sendReceiptEmail(user.email, invoice.id, user.firstName, user.locale)
}
}Keep the EmailService as the public surface. MailerSendService is an implementation detail — do not inject it directly into business modules.
Configuration
Environment variables
Required in production (apps/api/.env):
MAILERSEND_API_KEY="ms_prod_xxxxxxxxxxxxxxxxxxxxxxxx"
MAILERSEND_SENDER_EMAIL="noreply@myapp.com"
MAILERSEND_SENDER_NAME="My SaaS App"The FRONTEND_URL variable (shared across the API) is used to build the confirmation / reset / invitation links:
FRONTEND_URL="https://app.myapp.com"Test mode
apps/api/.env.test is populated with a deterministic fake key so the test suite never hits MailerSend:
MAILERSEND_API_KEY="ms_test_fake_key_12345abcdef67890ghijklmnopqrstuvwxyz"
MAILERSEND_SENDER_EMAIL="noreply@myapp.com"
MAILERSEND_SENDER_NAME="My SaaS App"The E2E suite stubs the MailerSend HTTP call; you can spy on it to assert an email was queued without sending anything.
CI / deployment
.github/workflows/deployment.yml references the API key as a GitHub Actions secret. Set it once in your repo settings:
gh secret set MAILERSEND_API_KEY --body "$MAILERSEND_KEY"The sender email and name are committed to the workflow file (they are not secrets). Change them there if your brand changes.
Templates
Each flow ships two templates (HTML + plain text) and two locale files (EN + FR):
apps/api/src/modules/email/
├── locales/
│ ├── en.ts # English strings (subjects, button labels, …)
│ └── fr.ts # French strings
├── templates/
│ ├── account-confirmation/
│ │ ├── html.template.ts
│ │ └── text.template.ts
│ ├── password-reset/
│ │ ├── html.template.ts
│ │ └── text.template.ts
│ └── invitation/
│ ├── html.template.ts
│ └── text.template.ts
└── services/
├── email.service.ts # Orchestrates render + send
├── mailersend.service.ts # Added by the module
└── translation.service.ts # Picks strings by user localeEditing a template
- Edit the
html.template.tsand/ortext.template.tsfor the flow. - Add any new translation keys to both
locales/en.tsandlocales/fr.ts. - Run the unit test:
npm run test:unit -- email.
Adding a new locale
- Create
locales/es.ts(or whichever locale) alongsideen.tsandfr.ts. - Import it in
translation.service.tsand extend the dispatch map. - Add the locale to the Prisma
Localeenum inprisma/schema/user.prisma, then runnpx prisma db pushto sync the enum into the dev DB.
Local development without MailerSend
The module is optional on purpose. If you do not install it, every auth flow still works — the rendered HTML and text are console.loged in the API server window. Use this during feature development; install the module when you need real deliverability (staging, production, or when you want to click through a real link).
To force the dev-log behaviour even with the module installed (e.g. on a feature branch where you do not want to send real email), point MAILERSEND_API_KEY at the fake test key:
MAILERSEND_API_KEY="ms_test_fake_key_12345abcdef67890ghijklmnopqrstuvwxyz"MailerSend will reject the key, the sendEmail() call will throw, and your logs will show the failure — you will still see the rendered HTML in the debug log.
MailerSend account setup
- Create an account: https://www.mailersend.com/signup
- Verify a domain under "Domains" — MailerSend will not deliver without a verified sender domain.
- Create an API token under "Integrations → API tokens". Scope it to "Send email" for principle-of-least-privilege.
- Add a sender identity: use
noreply@<your-verified-domain>or similar. This is whatMAILERSEND_SENDER_EMAILpoints at. - Paste the token into
sf new/sf update --mailersend-api-key.
Troubleshooting
Emails don't arrive in production
Check the API server logs for MailerSendService entries:
MailerSend error: ...→ inspect the error body. The two common causes are an unverified sender domain and a rate-limited API key.- No log line at all → the module may not be installed. Grep for
MailerSendServiceinemail.module.ts. If it is absent, runsf update --add-modules email .... Message ID: ...was logged but the mail is still missing → check the MailerSend dashboard "Activity" view. The recipient may have bounced or marked a previous send as spam.
"Failed to send email: fetch failed"
The MailerSend API is unreachable from your server. Check:
- Outbound HTTPS to
api.mailersend.comis allowed (corporate proxies, VPC egress rules). MAILERSEND_API_KEYis set (notundefinedor a placeholder). The log on service init prints the first 5 chars of the key — if it showsundefi, the env var is missing.
Test emails are being sent against a real account
You are running the non-test config against a live key. Verify NODE_ENV=test or your runner loads .env.test, not .env. The scaffolded Jest config already does this; if you changed it, switch back.
Templates look broken
Run the HTML through a real email client — inline CSS rules work differently than in a browser. MailerSend's preview (dashboard → "Email preview") and mail-tester.com are good sanity checks before you push template changes.
What happens when you run sf update on an email-enabled project
Because the installer uncomments markers and injects imports, the post-install state of several files differs from the untouched blueprint. The three-way merge in sf update handles this the same as any other file: if you edit email.service.ts after install, subsequent updates treat your version as user-modified and will propose conflicts rather than re-applying the uncommenting.
The clean way to upgrade: keep your template customisations in the locales/ and templates/ folders (which the installer never touches), not in email.service.ts — the service file should stay close to the blueprint so template updates apply cleanly.
Next steps
- Storage Module — file uploads paired with email links
- Module System — how modules compose
- Updating Projects — propagating template changes to your project