Email Setup
Configure Resend or Nodemailer for transactional emails. Gmail setup, DNS records (SPF, DKIM, DMARC), and 6 React Email templates.
LaunchFst uses a factory pattern for email delivery. Set EMAIL_PROVIDER to switch providers. When unset, emails log to the console — useful for local dev without any email service.
| Provider | Env value | Best for |
|---|---|---|
| Resend | resend | Production — best deliverability, analytics dashboard |
| Nodemailer | nodemailer | Gmail, custom SMTP, local testing with Mailtrap |
| Console fallback | (unset) | Local dev — logs to console, nothing actually sends |
Provider 1: Resend (Recommended for Production)
Resend is the easiest way to send transactional emails with excellent deliverability. Free tier: 3,000 emails/month.
Add environment variables
EMAIL_PROVIDER="resend"
RESEND_API_KEY="re_xxxxxxxxx"
EMAIL_FROM="YourSaaS <noreply@yourdomain.com>"Get your API key: resend.com → API Keys → Create Key.
Verify your domain
In the Resend dashboard → Domains → Add Domain. Resend gives you DNS records to add. See the DNS Records section below for exactly what to add.
EMAIL_FROM must use your verified domain — not gmail.com or outlook.com.
Test it
Trigger a signup to send the welcome email, or run:
pnpm email:previewOpens the React Email preview server at http://localhost:3001.
Provider 2: Nodemailer (Gmail / Custom SMTP)
Nodemailer works with any SMTP server: Gmail, Mailtrap, Amazon SES, SendGrid, or your own.
EMAIL_PROVIDER="nodemailer"
SMTP_HOST="smtp.yourprovider.com"
SMTP_PORT="587"
SMTP_USER="your@email.com"
SMTP_PASS="yourpassword"
SMTP_SECURE="false" # true for port 465
EMAIL_FROM="YourSaaS <noreply@yourdomain.com>"Gmail Setup
Gmail requires an App Password — your regular Google password won't work.
Enable 2-Step Verification
Go to myaccount.google.com/security → Turn on 2-Step Verification.
This is required before you can create App Passwords.
Create an App Password
Go to myaccount.google.com/apppasswords.
- App name:
YourSaaS(or anything) - Click Create
- Copy the 16-character password — spaces don't matter, remove them
Set environment variables
EMAIL_PROVIDER="nodemailer"
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="you@gmail.com"
SMTP_PASS="abcdefghijklmnop"
SMTP_SECURE="false"
EMAIL_FROM="YourSaaS <you@gmail.com>"Gmail limits: 500 emails/day for regular Gmail, 2,000/day for Google Workspace. For production, use Resend or Amazon SES instead.
Mailtrap (Development Testing)
Mailtrap catches all emails in a test inbox — nothing actually sends. Perfect for development.
Sign up and get credentials
Sign up at mailtrap.io → Email Testing → Inboxes → My Inbox → Show Credentials → select Nodemailer integration.
Set environment variables
EMAIL_PROVIDER="nodemailer"
SMTP_HOST="sandbox.smtp.mailtrap.io"
SMTP_PORT="587"
SMTP_USER="your-mailtrap-username"
SMTP_PASS="your-mailtrap-password"
SMTP_SECURE="false"
EMAIL_FROM="YourSaaS <noreply@yoursaas.com>"Other SMTP Providers
| Provider | SMTP_HOST | SMTP_PORT | Notes |
|---|---|---|---|
| Gmail | smtp.gmail.com | 587 | Requires App Password (see above) |
| Outlook/Hotmail | smtp-mail.outlook.com | 587 | Use your Microsoft password |
| Amazon SES | email-smtp.us-east-1.amazonaws.com | 587 | Requires IAM SMTP credentials |
| Resend (via SMTP) | smtp.resend.com | 465 | Username: resend, password: your API key |
| SendGrid | smtp.sendgrid.net | 587 | Username: apikey, password: your API key |
| Postmark | smtp.postmarkapp.com | 587 | Use your server API token as both user and pass |
| Mailgun | smtp.mailgun.org | 587 | Use SMTP credentials from Mailgun dashboard |
Sending Emails
sendTemplateEmail (recommended)
import { sendTemplateEmail } from "@/lib/email"
import { WelcomeEmail } from "@/components/core/emails/welcome"
await sendTemplateEmail(
"user@example.com",
WelcomeEmail,
{ name: "Alice", appName: "YourSaaS", appUrl: "https://yoursaas.com" },
"Welcome to YourSaaS!"
)Renders the React Email component to HTML and sends it via the configured provider.
getEmailProvider directly
import { getEmailProvider } from "@/lib/email"
await getEmailProvider().sendEmail({
to: "user@example.com",
subject: "Hello",
html: "<p>Hello world</p>",
})Guard with isEmailConfigured
import { isEmailConfigured } from "@/lib/email"
if (isEmailConfigured()) {
await sendTemplateEmail(...)
}Returns false when EMAIL_PROVIDER is not set — useful for non-critical emails you want to skip in development.
Email Templates
All 6 templates live in components/core/emails/ and are built with @react-email/components.
1. welcome.tsx — WelcomeEmail
Sent on signup. Shows 3 getting-started steps and a "Go to Dashboard" button.
Props:
{
name: string // User's display name
appName?: string // Defaults to "YourSaaS"
appUrl?: string // Defaults to "http://localhost:3000"
}Triggered by: POST /api/auth/signup
2. password-reset.tsx — PasswordResetEmail
Contains a reset link with a 1-hour expiry.
Props:
{
name: string // User's display name
resetUrl: string // Full reset URL with token
appName?: string
appUrl?: string
}Triggered by: POST /api/auth/forgot-password
3. org-invitation.tsx — OrgInvitationEmail
Sent when a member is invited to an organization. Invitation expires in 7 days.
Props:
{
inviterName: string // Name of the person sending the invite
orgName: string // Organization name
role: string // Role being granted (ADMIN, MEMBER)
inviteUrl: string // Full accept URL with token
appName?: string
appUrl?: string
}Triggered by: POST /api/organizations/[orgId]/invitations
4. subscription-confirmed.tsx — SubscriptionConfirmedEmail
Sent after a successful payment webhook confirms a new subscription.
Props:
{
name: string // User's display name
planName: string // e.g. "Pro"
billingPeriod: string // e.g. "Monthly" or "Annual"
nextRenewalDate: string // Human-readable date string
appName?: string
appUrl?: string
}5. receipt.tsx — ReceiptEmail
Payment receipt with amount, date, plan, and payment method.
Props:
{
name: string // User's display name
amount: string // e.g. "$29.00"
date: string // e.g. "March 31, 2026"
planName: string // e.g. "Pro (Monthly)"
paymentMethod: string // e.g. "Visa •••• 4242"
appName?: string
appUrl?: string
}6. _layout.tsx — EmailLayout
The base layout used by all templates. Renders a header bar with the app name, a white content area, and a footer.
Props:
{
preview: string // Text shown in email client inbox preview snippet
children: ReactNode // Template-specific content
appName?: string
appUrl?: string
}All templates wrap their content in <EmailLayout> — do the same when adding new templates.
Adding a New Template
- Create
components/core/emails/my-template.tsx:
import { Text, Section } from "@react-email/components"
import { EmailLayout } from "./_layout"
interface MyEmailProps {
name: string
appName?: string
appUrl?: string
}
export function MyEmail({ name, appName = "YourSaaS", appUrl = "http://localhost:3000" }: MyEmailProps) {
return (
<EmailLayout preview="Your inbox preview text here" appName={appName} appUrl={appUrl}>
<Text>Hello {name}!</Text>
</EmailLayout>
)
}
export default MyEmail- Send it:
import { sendTemplateEmail } from "@/lib/email"
import { MyEmail } from "@/components/core/emails/my-template"
await sendTemplateEmail(user.email, MyEmail, { name: user.name }, "Your subject line")Previewing Templates
pnpm email:previewOpens the React Email development server at http://localhost:3001. All templates in components/core/emails/ are listed and previewable in your browser.
DNS Records (Production Deliverability)
Without proper DNS records, your emails will land in spam. These records prove you own the domain and are authorized to send from it.
You need three types: SPF, DKIM, and DMARC.
Where to Add DNS Records
Add records in your domain registrar or DNS provider:
- Cloudflare → DNS → Add Record
- Namecheap → Advanced DNS → Add Record
- Vercel Domains → DNS Records tab
- GoDaddy → DNS Management → Add Record
DNS changes can take up to 48 hours to propagate, but usually work within 5–30 minutes.
SPF Record
SPF tells receiving mail servers which services are authorized to send email from your domain.
| Type | Name/Host | Value |
|---|---|---|
| TXT | @ | See below |
Resend only:
v=spf1 include:_spf.resend.com -allGmail only:
v=spf1 include:_spf.google.com -allResend + Gmail:
v=spf1 include:_spf.resend.com include:_spf.google.com -allAmazon SES:
v=spf1 include:amazonses.com -allYou can only have one SPF record per domain. Combine multiple senders into a single record using multiple include: statements.
DKIM Record
DKIM adds a cryptographic signature to every outgoing email, proving it hasn't been tampered with.
If using Resend:
Resend provides 3 CNAME records in the dashboard → Domains → your domain:
| Type | Name/Host | Value |
|---|---|---|
| CNAME | resend._domainkey | (provided by Resend) |
| CNAME | resend2._domainkey | (provided by Resend) |
| CNAME | resend3._domainkey | (provided by Resend) |
If using Gmail/Google Workspace:
Google Admin → Apps → Gmail → Authenticate Email → Generate New Record. Add the TXT record Google gives you:
| Type | Name/Host | Value |
|---|---|---|
| TXT | google._domainkey | (provided by Google Admin) |
DMARC Record
DMARC tells receiving servers what to do when SPF or DKIM checks fail. It also sends you aggregate reports about your email sending patterns.
| Type | Name/Host | Value |
|---|---|---|
| TXT | _dmarc | See below |
Start with monitoring (recommended for first 2–4 weeks):
v=DMARC1; p=none; rua=mailto:you@yourdomain.com; pct=100Reports are sent to you but no emails are blocked. Use this to verify everything is working.
After confirming — switch to quarantine:
v=DMARC1; p=quarantine; rua=mailto:you@yourdomain.com; pct=100Failed emails go to spam.
Maximum protection:
v=DMARC1; p=reject; rua=mailto:you@yourdomain.com; pct=100Failed emails are rejected outright.
Replace you@yourdomain.com with an address where you want to receive DMARC reports.
Complete DNS Example (Resend)
Here's what a fully configured domain looks like:
| Type | Name | Value |
|---|---|---|
| TXT | @ | v=spf1 include:_spf.resend.com -all |
| CNAME | resend._domainkey | (from Resend dashboard) |
| CNAME | resend2._domainkey | (from Resend dashboard) |
| CNAME | resend3._domainkey | (from Resend dashboard) |
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:you@yourdomain.com; pct=100 |
Verify Your DNS Records
Using terminal:
# Check SPF
dig TXT yourdomain.com
# Check DKIM (Resend example)
dig CNAME resend._domainkey.yourdomain.com
# Check DMARC
dig TXT _dmarc.yourdomain.comUsing online tools:
- MXToolbox SuperTool — comprehensive DNS check
- mail-tester.com — send a test email, get a spam score out of 10
- Resend Dashboard → Domains — shows green checkmarks when records are verified
Testing Email
Full Test Checklist
| How to Trigger | What to Verify | |
|---|---|---|
| Welcome | Sign up with a new account | Arrives, links work, branding correct |
| Password reset | Click "Forgot password" | Arrives, link works, token expires after 1 hour |
| Org invitation | Org settings → Invite member | Arrives, accept link works |
| Subscription confirmed | Complete a test checkout | Arrives with correct plan name |
Gmail Aliases for Testing
Gmail ignores dots and supports + aliases — all of these deliver to you@gmail.com but create separate app accounts:
you+test1@gmail.com
you+test2@gmail.com
y.o.u@gmail.comCommon Issues
| Problem | Cause | Fix |
|---|---|---|
| "Invalid login" with Gmail | App password wrong or 2FA not enabled | Create new App Password at myaccount.google.com/apppasswords |
| Emails go to spam | Missing DNS records | Add SPF, DKIM, DMARC records |
| "Connection refused" | Wrong SMTP_HOST or SMTP_PORT | Check your provider's SMTP settings |
| "Greeting never received" | Port/secure mismatch | Port 587 + SMTP_SECURE=false, or port 465 + SMTP_SECURE=true |
| Email not arriving | Spam folder | Check spam; then check terminal logs for errors |
EMAIL_FROM rejected | Address doesn't match verified domain | Resend: EMAIL_FROM must use your verified domain |
| Console fallback runs | EMAIL_PROVIDER not set | Set EMAIL_PROVIDER="resend" or "nodemailer" in .env.local |
Environment Variables Reference
| Variable | Required | Description |
|---|---|---|
EMAIL_PROVIDER | No* | "resend" or "nodemailer". Unset = console only. |
EMAIL_FROM | Yes (if set) | Sender address, e.g. YourSaaS <noreply@yourdomain.com> |
RESEND_API_KEY | Resend only | API key from resend.com |
SMTP_HOST | Nodemailer only | e.g. smtp.gmail.com |
SMTP_PORT | Nodemailer only | 587 (default) or 465 |
SMTP_USER | Nodemailer only | SMTP username / email |
SMTP_PASS | Nodemailer only | SMTP password or App Password |
SMTP_SECURE | Nodemailer only | "true" for port 465, "false" for port 587 |
Production Recommendations
| Stage | Provider | Reason |
|---|---|---|
| Local dev | None (console) or Mailtrap | No config needed; Mailtrap catches real sends |
| Staging | Gmail or Mailtrap | Quick setup, verify templates work |
| Production | Resend or Amazon SES | Reliable deliverability, analytics, no daily limits |
Always use a custom domain in production (noreply@yoursaas.com, not yoursaas@gmail.com). Custom domains with proper DNS records have significantly better inbox placement.