Email Infrastructure for Your SaaS: Transactional Emails, Deliverability, and Automation
Email remains the backbone of SaaS communication. Password resets, invoices, onboarding sequences, usage alerts — your users depend on these messages arriving reliably. Yet many SaaS founders treat email as an afterthought, only to discover their critical messages are landing in spam folders or not arriving at all.
This guide walks you through building a robust email infrastructure that scales with your SaaS, from choosing the right providers to implementing deliverability best practices and building automated sequences.
Why Email Infrastructure Matters
Consider what happens when email fails in a SaaS:
- Password reset emails don't arrive → support tickets skyrocket
- Invoice emails land in spam → payments are delayed
- Onboarding sequences are ignored → activation rates plummet
- Usage alerts go missing → users hit limits without warning
A study by ReturnPath found that 21% of legitimate emails never reach the inbox. For a SaaS with 10,000 users, that's 2,100 people potentially missing critical communications.
Transactional vs. Marketing Email: Why You Need Both (Separately)
The first architectural decision is separating transactional and marketing email streams.
Transactional Emails
These are triggered by user actions and expected immediately:
- Password resets
- Email verification
- Payment confirmations
- Usage notifications
- Team invitations
Marketing Emails
These are sent in bulk on a schedule:
- Product updates
- Newsletter
- Feature announcements
- Re-engagement campaigns
Why separate them? If your marketing emails generate spam complaints (inevitable at scale), they can tank the reputation of your sending domain. Suddenly your password reset emails land in spam too.
# Recommended domain setup
transactional: mail.yourapp.com → Postmark / AWS SES
marketing: updates.yourapp.com → Resend / SendGrid
Choosing an Email Provider
Here's a practical comparison for SaaS use cases:
Postmark
Best for: Pure transactional email with industry-leading deliverability.
- Dedicated transactional infrastructure (no marketing allowed)
- Excellent delivery speeds (< 1 second average)
- Built-in bounce and spam complaint handling
- Pricing: from $15/month for 10,000 emails
AWS SES
Best for: High volume at low cost.
- $0.10 per 1,000 emails
- Requires more setup and monitoring
- No built-in template management
- Great if you're already on AWS
Resend
Best for: Developer experience and modern API.
- React Email for templating
- Excellent DX with TypeScript SDK
- Webhook support for delivery tracking
- Pricing: from $20/month for 50,000 emails
SendGrid / Mailgun
Best for: Combined transactional + marketing needs.
- Feature-rich but can be complex
- Marketing features built in
- Larger learning curve
Setting Up DNS for Deliverability
Before sending a single email, configure these DNS records:
SPF (Sender Policy Framework)
Tells receiving servers which IPs are allowed to send email for your domain.
yourapp.com. IN TXT "v=spf1 include:spf.postmarkapp.com include:amazonses.com ~all"
DKIM (DomainKeys Identified Mail)
Cryptographically signs your emails so receivers can verify they haven't been tampered with.
postmark._domainkey.yourapp.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCS..."
DMARC (Domain-based Message Authentication)
Tells receiving servers what to do with emails that fail SPF/DKIM checks.
_dmarc.yourapp.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourapp.com; pct=100"
Return-Path / Bounce Domain
Configure a custom return-path so bounces are handled by your provider:
pm-bounces.yourapp.com. IN CNAME pm.mtasv.net
Pro tip: Start with p=none for DMARC while monitoring, then escalate to p=quarantine and eventually p=reject once you're confident all legitimate email is properly authenticated.
Building a Transactional Email Service
Here's a production-ready email service in TypeScript:
// lib/email/email-service.ts
import { Resend } from 'resend';
interface EmailOptions {
to: string | string[];
subject: string;
template: string;
data: Record<string, unknown>;
tags?: { name: string; value: string }[];
}
class EmailService {
private client: Resend;
private fromAddress: string;
constructor() {
this.client = new Resend(process.env.RESEND_API_KEY);
this.fromAddress = process.env.EMAIL_FROM || 'noreply@yourapp.com';
}
async send(options: EmailOptions): Promise<{ id: string }> {
const html = await this.renderTemplate(options.template, options.data);
const result = await this.client.emails.send({
from: this.fromAddress,
to: Array.isArray(options.to) ? options.to : [options.to],
subject: options.subject,
html,
tags: options.tags,
headers: {
'X-Entity-Ref-ID': crypto.randomUUID(), // Prevents threading in Gmail
},
});
if (result.error) {
console.error('Email send failed:', result.error);
throw new Error(`Email send failed: ${result.error.message}`);
}
return { id: result.data!.id };
}
private async renderTemplate(
template: string,
data: Record<string, unknown>
): Promise<string> {
// Use React Email, Handlebars, or any templating engine
const { render } = await import('@react-email/render');
const templates = await import('./templates');
const Component = templates[template];
if (!Component) {
throw new Error(`Unknown email template: ${template}`);
}
return render(Component(data));
}
}
export const emailService = new EmailService();
Using the Service
// Send a password reset email
await emailService.send({
to: user.email,
subject: 'Reset your password',
template: 'PasswordReset',
data: {
name: user.name,
resetUrl: `https://yourapp.com/reset?token=${token}`,
expiresIn: '1 hour',
},
tags: [{ name: 'category', value: 'transactional' }],
});
Handling Bounces and Complaints
Ignoring bounces and spam complaints is the fastest way to destroy your sender reputation.
// api/webhooks/email/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function POST(req: NextRequest) {
const event = await req.json();
switch (event.RecordType) {
case 'Bounce':
await handleBounce(event);
break;
case 'SpamComplaint':
await handleComplaint(event);
break;
case 'SubscriptionChange':
await handleUnsubscribe(event);
break;
}
return NextResponse.json({ received: true });
}
async function handleBounce(event: any) {
const { Email, Type } = event;
if (Type === 'HardBounce') {
// Hard bounce: email address doesn't exist
// IMMEDIATELY stop sending to this address
await db.user.update({
where: { email: Email },
data: {
emailBounced: true,
emailBouncedAt: new Date(),
},
});
console.warn(`Hard bounce for ${Email} — marked as bounced`);
}
}
async function handleComplaint(event: any) {
// Someone marked your email as spam
// You MUST suppress this address
await db.emailSuppression.create({
data: {
email: event.Email,
reason: 'spam_complaint',
occurredAt: new Date(),
},
});
}
Critical: Always check your suppression list before sending:
async function canSendTo(email: string): Promise<boolean> {
const suppression = await db.emailSuppression.findFirst({
where: { email },
});
const user = await db.user.findUnique({
where: { email },
select: { emailBounced: true },
});
return !suppression && !user?.emailBounced;
}
Building Automated Email Sequences
Automated sequences are where email becomes a growth engine. Here's how to build a flexible drip campaign system:
// lib/email/sequences.ts
interface SequenceStep {
delayHours: number;
template: string;
subject: string;
condition?: (user: User) => boolean;
}
const onboardingSequence: SequenceStep[] = [
{
delayHours: 0,
template: 'Welcome',
subject: 'Welcome to YourApp! Here\'s how to get started',
},
{
delayHours: 24,
template: 'GettingStarted',
subject: 'Did you set up your first project?',
condition: (user) => !user.hasCreatedProject,
},
{
delayHours: 72,
template: 'FeatureHighlight',
subject: '3 features you might have missed',
condition: (user) => user.loginCount < 3,
},
{
delayHours: 168, // 7 days
template: 'FeedbackRequest',
subject: 'How\'s your first week been?',
},
];
Processing the Sequence with a Cron Job
// jobs/process-email-sequences.ts
async function processSequences() {
const activeEnrollments = await db.emailSequenceEnrollment.findMany({
where: {
status: 'active',
nextStepAt: { lte: new Date() },
},
include: { user: true },
});
for (const enrollment of activeEnrollments) {
const sequence = getSequence(enrollment.sequenceName);
const step = sequence[enrollment.currentStep];
if (!step) {
// Sequence complete
await db.emailSequenceEnrollment.update({
where: { id: enrollment.id },
data: { status: 'completed' },
});
continue;
}
// Check condition
if (step.condition && !step.condition(enrollment.user)) {
// Skip this step, move to next
await advanceToNextStep(enrollment, sequence);
continue;
}
// Check suppression
if (!(await canSendTo(enrollment.user.email))) {
await db.emailSequenceEnrollment.update({
where: { id: enrollment.id },
data: { status: 'suppressed' },
});
continue;
}
// Send the email
await emailService.send({
to: enrollment.user.email,
subject: step.subject,
template: step.template,
data: { name: enrollment.user.name },
tags: [
{ name: 'sequence', value: enrollment.sequenceName },
{ name: 'step', value: String(enrollment.currentStep) },
],
});
await advanceToNextStep(enrollment, sequence);
}
}
Monitoring Email Health
Track these metrics to catch problems early:
Key Metrics to Monitor
| Metric | Healthy Range | Action if Outside |
|---|---|---|
| Delivery rate | > 98% | Check DNS, review content |
| Bounce rate | < 2% | Clean your list, verify addresses |
| Spam complaint rate | < 0.1% | Review content, check frequency |
| Open rate (transactional) | > 60% | Check subject lines, sender name |
| Unsubscribe rate | < 0.5% | Reduce frequency, improve content |
Setting Up Alerts
// lib/email/monitoring.ts
async function checkEmailHealth() {
const stats = await emailProvider.getStats({ period: '24h' });
const bounceRate = stats.bounced / stats.sent;
const complaintRate = stats.complaints / stats.sent;
if (bounceRate > 0.05) {
await alertOps('High bounce rate', {
rate: bounceRate,
period: '24h',
action: 'Check email list hygiene',
});
}
if (complaintRate > 0.001) {
await alertOps('High spam complaint rate', {
rate: complaintRate,
period: '24h',
action: 'Review recent email content and frequency',
});
}
}
Email Templates That Work
A few principles for SaaS email templates:
1. Keep It Simple
Plain text emails often outperform heavily designed ones for transactional messages. Gmail's Promotions tab actively looks for HTML-heavy emails.
2. One Call-to-Action
Each email should have exactly one primary action:
<!-- Good: Single clear CTA -->
<a href="{{resetUrl}}" style="
background-color: #4F46E5;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
display: inline-block;
">
Reset Your Password
</a>
<!-- Also include a plain text fallback link -->
<p style="color: #6B7280; font-size: 14px;">
Or copy this link: {{resetUrl}}
</p>
3. Include an Unsubscribe Link (Even for Transactional)
While not legally required for transactional emails, it prevents spam complaints:
headers: {
'List-Unsubscribe': '<https://yourapp.com/unsubscribe?token=...>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
}
Common Pitfalls to Avoid
- Using a free email as the sender (gmail.com, outlook.com) — always use your own domain
- Not warming up a new domain — start with low volume and gradually increase over 2-4 weeks
- Sending to unverified addresses — implement double opt-in for marketing emails
- Ignoring bounces — they compound and destroy your reputation
- One provider for everything — separate transactional and marketing streams
- No retry logic — implement exponential backoff for failed sends
- Missing
X-Entity-Ref-IDheader — without it, Gmail may thread unrelated emails together
Production Checklist
Before going live, verify:
- SPF, DKIM, and DMARC records configured and passing
- Separate sending domains for transactional and marketing
- Bounce and complaint webhooks connected and processing
- Suppression list checked before every send
- Email templates tested across Gmail, Outlook, and Apple Mail
- Unsubscribe mechanism working (one-click for marketing)
- Monitoring and alerting set up for delivery metrics
- Domain warmup plan for new sending domains
- Retry logic with exponential backoff implemented
- Rate limiting to avoid provider throttling
Conclusion
Email infrastructure isn't glamorous, but it's one of the most critical systems in your SaaS. Users expect transactional emails to arrive instantly and reliably. When they don't, trust erodes fast.
Start simple: pick a reliable transactional provider like Postmark or Resend, configure your DNS properly, and handle bounces from day one. As you grow, add automated sequences and monitoring. The investment pays off in fewer support tickets, better activation rates, and higher user trust.
Your email infrastructure is the silent workhorse of your SaaS. Treat it with the respect it deserves.