Back to blog
emailsaasinfrastructuredeliverabilityautomationtransactional-email

Email Infrastructure for Your SaaS: Transactional Emails, Deliverability, and Automation

By SaaS Masters18 maart 202610 min read

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

MetricHealthy RangeAction 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

  1. Using a free email as the sender (gmail.com, outlook.com) — always use your own domain
  2. Not warming up a new domain — start with low volume and gradually increase over 2-4 weeks
  3. Sending to unverified addresses — implement double opt-in for marketing emails
  4. Ignoring bounces — they compound and destroy your reputation
  5. One provider for everything — separate transactional and marketing streams
  6. No retry logic — implement exponential backoff for failed sends
  7. Missing X-Entity-Ref-ID header — 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.