Email is still the highest-ROI marketing channel for SaaS. But most developers treat it as an afterthought — a Resend integration for transactional emails and nothing else. These patterns build an email system that drives activation, retention, and expansion revenue.
Transactional vs Marketing Email
Use separate sending domains:
- Transactional (notifications@app.com): password resets, receipts, alerts — high deliverability required
- Marketing (hello@app.com): newsletters, drip campaigns, announcements — can tolerate some filtering
Never send marketing email from your transactional domain. One spam complaint tanks deliverability for both.
React Email Templates
npm install @react-email/components react-email resend
// emails/WelcomeEmail.tsx
import {
Body, Button, Container, Head, Heading,
Html, Preview, Section, Text
} from '@react-email/components'
interface WelcomeEmailProps {
name: string
onboardingUrl: string
}
export function WelcomeEmail({ name, onboardingUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to the app, {name}</Preview>
<Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f9fafb' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
<Heading>Welcome, {name}</Heading>
<Text>You're all set. Here's what to do next:</Text>
<Section>
<Text>1. Complete your profile</Text>
<Text>2. Connect your first integration</Text>
<Text>3. Invite a teammate</Text>
</Section>
<Button href={onboardingUrl} style={{
backgroundColor: '#3b82f6',
color: 'white',
padding: '12px 24px',
borderRadius: '6px',
textDecoration: 'none',
}}>
Get started
</Button>
</Container>
</Body>
</Html>
)
}
Sending with Resend
// lib/email.ts
import { Resend } from 'resend'
import { render } from '@react-email/render'
const resend = new Resend(process.env.RESEND_API_KEY)
export async function sendWelcomeEmail(user: { email: string; name: string }) {
const html = render(WelcomeEmail({
name: user.name,
onboardingUrl: `https://app.example.com/onboard`,
}))
await resend.emails.send({
from: 'Atlas <hello@whoffagents.com>',
to: user.email,
subject: `Welcome to Whoff Agents, ${user.name}`,
html,
})
}
Lifecycle Email Sequences
Map emails to user lifecycle stages:
Day 0: Welcome + onboarding CTA (transactional)
Day 1: Tip #1 -- most common first action
Day 3: Check-in -- did they complete onboarding?
Day 7: Social proof -- what other users built
Day 14: Upgrade prompt (if on free tier)
Day 30: Monthly digest -- their usage + new features
Scheduling with BullMQ
// Queue delayed emails on signup
async function scheduleOnboardingSequence(userId: string, email: string) {
const delays = [
{ template: 'tip-1', delay: 1 * 24 * 60 * 60 * 1000 }, // Day 1
{ template: 'check-in', delay: 3 * 24 * 60 * 60 * 1000 }, // Day 3
{ template: 'social-proof', delay: 7 * 24 * 60 * 60 * 1000 }, // Day 7
{ template: 'upgrade', delay: 14 * 24 * 60 * 60 * 1000 }, // Day 14
]
for (const { template, delay } of delays) {
await emailQueue.add(template, { userId, email }, {
delay,
jobId: `${template}:${userId}`, // prevent duplicates
})
}
}
// Cancel remaining emails if user upgrades
async function cancelUpgradeEmail(userId: string) {
const job = await emailQueue.getJob(`upgrade:${userId}`)
await job?.remove()
}
Tracking Opens and Clicks
Resend provides webhooks for email events:
// app/api/webhooks/resend/route.ts
export async function POST(req: Request) {
const event = await req.json()
switch (event.type) {
case 'email.opened':
await db.emailEvent.create({
data: { emailId: event.data.email_id, type: 'opened', createdAt: new Date() }
})
break
case 'email.clicked':
await db.emailEvent.create({
data: { emailId: event.data.email_id, type: 'clicked', url: event.data.click.link }
})
break
case 'email.bounced':
await db.user.update({
where: { email: event.data.to[0] },
data: { emailBounced: true },
})
break
}
return Response.json({ ok: true })
}
The AI SaaS Starter at whoffagents.com ships with Resend configured, React Email welcome template, and BullMQ email queue pre-wired. Add your API key and your lifecycle emails are ready to go. $99 one-time.
Top comments (0)