E-mailinfrastructuur voor je SaaS: transactionele e-mails, deliverability en automatisering
E-mail blijft de ruggengraat van SaaS-communicatie. Wachtwoord-resets, facturen, onboarding-sequences, gebruiksnotificaties — je gebruikers zijn afhankelijk van het betrouwbaar aankomen van deze berichten. Toch behandelen veel SaaS-founders e-mail als een bijzaak, om er vervolgens achter te komen dat hun kritische berichten in spamfolders belanden of helemaal niet aankomen.
Deze gids begeleidt je bij het bouwen van een robuuste e-mailinfrastructuur die meeschaalt met je SaaS, van het kiezen van de juiste providers tot het implementeren van deliverability-best-practices en het bouwen van geautomatiseerde sequences.
Waarom e-mailinfrastructuur ertoe doet
Bedenk wat er gebeurt als e-mail faalt in een SaaS:
- Wachtwoord-reset e-mails komen niet aan → supporttickets schieten omhoog
- Factuur-e-mails belanden in spam → betalingen worden vertraagd
- Onboarding-sequences worden genegeerd → activatiepercentages kelderen
- Gebruiksnotificaties verdwijnen → gebruikers bereiken limieten zonder waarschuwing
Onderzoek van ReturnPath toont aan dat 21% van legitieme e-mails de inbox nooit bereikt. Voor een SaaS met 10.000 gebruikers zijn dat 2.100 mensen die mogelijk kritische communicatie missen.
Transactioneel vs. marketing e-mail: waarom je beide nodig hebt (apart)
De eerste architecturale beslissing is het scheiden van transactionele en marketing e-mailstromen.
Transactionele e-mails
Deze worden getriggerd door gebruikersacties en worden direct verwacht:
- Wachtwoord-resets
- E-mailverificatie
- Betalingsbevestigingen
- Gebruiksnotificaties
- Teamuitnodigingen
Marketing e-mails
Deze worden in bulk verstuurd volgens een schema:
- Productupdates
- Nieuwsbrief
- Feature-aankondigingen
- Re-engagement campagnes
Waarom scheiden? Als je marketing-e-mails spamklachten genereren (onvermijdelijk op schaal), kan dat de reputatie van je verzenddomein verwoesten. Plotseling belanden ook je wachtwoord-reset e-mails in de spam.
# Aanbevolen domeinopzet
transactioneel: mail.jeapp.nl → Postmark / AWS SES
marketing: updates.jeapp.nl → Resend / SendGrid
Een e-mailprovider kiezen
Hier is een praktische vergelijking voor SaaS-toepassingen:
Postmark
Beste voor: Puur transactionele e-mail met marktleidende deliverability.
- Dedicated transactionele infrastructuur (geen marketing toegestaan)
- Uitstekende levertijden (< 1 seconde gemiddeld)
- Ingebouwde bounce- en spamklachtafhandeling
- Prijzen: vanaf $15/maand voor 10.000 e-mails
AWS SES
Beste voor: Hoog volume tegen lage kosten.
- $0,10 per 1.000 e-mails
- Vereist meer setup en monitoring
- Geen ingebouwd templatebeheer
- Ideaal als je al op AWS zit
Resend
Beste voor: Developer experience en moderne API.
- React Email voor templating
- Uitstekende DX met TypeScript SDK
- Webhook-ondersteuning voor levering-tracking
- Prijzen: vanaf $20/maand voor 50.000 e-mails
SendGrid / Mailgun
Beste voor: Gecombineerde transactionele + marketing behoeften.
- Feature-rijk maar kan complex zijn
- Marketing-features ingebouwd
- Grotere leercurve
DNS instellen voor deliverability
Voordat je één e-mail verstuurt, configureer deze DNS-records:
SPF (Sender Policy Framework)
Vertelt ontvangende servers welke IP-adressen e-mail mogen versturen namens jouw domein.
jeapp.nl. IN TXT "v=spf1 include:spf.postmarkapp.com include:amazonses.com ~all"
DKIM (DomainKeys Identified Mail)
Ondertekent je e-mails cryptografisch zodat ontvangers kunnen verifiëren dat ze niet zijn aangepast.
postmark._domainkey.jeapp.nl. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCS..."
DMARC (Domain-based Message Authentication)
Vertelt ontvangende servers wat te doen met e-mails die SPF/DKIM-controles niet halen.
_dmarc.jeapp.nl. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@jeapp.nl; pct=100"
Return-Path / Bounce-domein
Configureer een aangepast return-path zodat bounces door je provider worden afgehandeld:
pm-bounces.jeapp.nl. IN CNAME pm.mtasv.net
Pro tip: Begin met p=none voor DMARC terwijl je monitort, escaleer dan naar p=quarantine en uiteindelijk p=reject zodra je zeker weet dat alle legitieme e-mail correct is geverifieerd.
Een transactionele e-mailservice bouwen
Hier is een productiewaardige e-mailservice 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@jeapp.nl';
}
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(), // Voorkomt threading in Gmail
},
});
if (result.error) {
console.error('E-mail verzenden mislukt:', result.error);
throw new Error(`E-mail verzenden mislukt: ${result.error.message}`);
}
return { id: result.data!.id };
}
private async renderTemplate(
template: string,
data: Record<string, unknown>
): Promise<string> {
const { render } = await import('@react-email/render');
const templates = await import('./templates');
const Component = templates[template];
if (!Component) {
throw new Error(`Onbekend e-mailtemplate: ${template}`);
}
return render(Component(data));
}
}
export const emailService = new EmailService();
De service gebruiken
// Verstuur een wachtwoord-reset e-mail
await emailService.send({
to: user.email,
subject: 'Stel je wachtwoord opnieuw in',
template: 'PasswordReset',
data: {
name: user.name,
resetUrl: `https://jeapp.nl/reset?token=${token}`,
expiresIn: '1 uur',
},
tags: [{ name: 'category', value: 'transactional' }],
});
Bounces en klachten afhandelen
Het negeren van bounces en spamklachten is de snelste manier om je afzenderreputatie te vernietigen.
// 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: e-mailadres bestaat niet
// STOP ONMIDDELLIJK met versturen naar dit adres
await db.user.update({
where: { email: Email },
data: {
emailBounced: true,
emailBouncedAt: new Date(),
},
});
console.warn(`Hard bounce voor ${Email} — gemarkeerd als bounced`);
}
}
async function handleComplaint(event: any) {
// Iemand heeft je e-mail als spam gemarkeerd
// Je MOET dit adres onderdrukken
await db.emailSuppression.create({
data: {
email: event.Email,
reason: 'spam_complaint',
occurredAt: new Date(),
},
});
}
Kritiek: Controleer altijd je suppressielijst voordat je verstuurt:
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;
}
Geautomatiseerde e-mailsequences bouwen
Geautomatiseerde sequences zijn waar e-mail een groeimotor wordt. Zo bouw je een flexibel drip-campagnesysteem:
// lib/email/sequences.ts
interface SequenceStep {
delayHours: number;
template: string;
subject: string;
condition?: (user: User) => boolean;
}
const onboardingSequence: SequenceStep[] = [
{
delayHours: 0,
template: 'Welcome',
subject: 'Welkom bij JeApp! Zo ga je van start',
},
{
delayHours: 24,
template: 'GettingStarted',
subject: 'Heb je al je eerste project aangemaakt?',
condition: (user) => !user.hasCreatedProject,
},
{
delayHours: 72,
template: 'FeatureHighlight',
subject: '3 features die je misschien hebt gemist',
condition: (user) => user.loginCount < 3,
},
{
delayHours: 168, // 7 dagen
template: 'FeedbackRequest',
subject: 'Hoe was je eerste week?',
},
];
De sequence verwerken met een cronjob
// 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 voltooid
await db.emailSequenceEnrollment.update({
where: { id: enrollment.id },
data: { status: 'completed' },
});
continue;
}
// Controleer voorwaarde
if (step.condition && !step.condition(enrollment.user)) {
// Sla deze stap over, ga naar de volgende
await advanceToNextStep(enrollment, sequence);
continue;
}
// Controleer suppressie
if (!(await canSendTo(enrollment.user.email))) {
await db.emailSequenceEnrollment.update({
where: { id: enrollment.id },
data: { status: 'suppressed' },
});
continue;
}
// Verstuur de e-mail
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);
}
}
E-mailgezondheid monitoren
Volg deze statistieken om problemen vroegtijdig te signaleren:
Belangrijke metrics om te monitoren
| Metric | Gezond bereik | Actie als erbuiten |
|---|---|---|
| Leveringspercentage | > 98% | Controleer DNS, review content |
| Bouncepercentage | < 2% | Schoon je lijst op, verifieer adressen |
| Spamklachtenpercentage | < 0,1% | Review content, controleer frequentie |
| Openingspercentage (transactioneel) | > 60% | Controleer onderwerpen, afzendernaam |
| Uitschrijfpercentage | < 0,5% | Verminder frequentie, verbeter content |
Alerts instellen
// 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('Hoog bouncepercentage', {
rate: bounceRate,
period: '24h',
action: 'Controleer e-maillijst hygiene',
});
}
if (complaintRate > 0.001) {
await alertOps('Hoog spamklachtenpercentage', {
rate: complaintRate,
period: '24h',
action: 'Review recente e-mailcontent en frequentie',
});
}
}
E-mailtemplates die werken
Enkele principes voor SaaS e-mailtemplates:
1. Houd het simpel
Platte tekst e-mails presteren vaak beter dan zwaar ontworpen e-mails voor transactionele berichten. De Promoties-tab van Gmail zoekt actief naar HTML-zware e-mails.
2. Eén call-to-action
Elke e-mail moet precies één primaire actie bevatten:
<!-- Goed: Eén duidelijke CTA -->
<a href="{{resetUrl}}" style="
background-color: #4F46E5;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
display: inline-block;
">
Stel je wachtwoord opnieuw in
</a>
<!-- Voeg ook een platte tekst fallback-link toe -->
<p style="color: #6B7280; font-size: 14px;">
Of kopieer deze link: {{resetUrl}}
</p>
3. Voeg een uitschrijflink toe (ook voor transactioneel)
Hoewel niet wettelijk vereist voor transactionele e-mails, voorkomt het spamklachten:
headers: {
'List-Unsubscribe': '<https://jeapp.nl/uitschrijven?token=...>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
}
Veelvoorkomende valkuilen om te vermijden
- Een gratis e-mailadres als afzender gebruiken (gmail.com, outlook.com) — gebruik altijd je eigen domein
- Een nieuw domein niet opwarmen — begin met laag volume en verhoog geleidelijk over 2-4 weken
- Versturen naar niet-geverifieerde adressen — implementeer dubbele opt-in voor marketing-e-mails
- Bounces negeren — ze stapelen op en vernietigen je reputatie
- Eén provider voor alles — scheid transactionele en marketing stromen
- Geen retry-logica — implementeer exponential backoff voor mislukte verzendingen
- Ontbrekende
X-Entity-Ref-IDheader — zonder kan Gmail niet-gerelateerde e-mails groeperen
Productiechecklist
Voordat je live gaat, verifieer:
- SPF-, DKIM- en DMARC-records geconfigureerd en werkend
- Aparte verzenddomeinen voor transactioneel en marketing
- Bounce- en klachtwebhooks aangesloten en verwerken
- Suppressielijst gecontroleerd voor elke verzending
- E-mailtemplates getest in Gmail, Outlook en Apple Mail
- Uitschrijfmechanisme werkt (one-click voor marketing)
- Monitoring en alerting ingesteld voor leveringsstatistieken
- Domeinopwarmplan voor nieuwe verzenddomeinen
- Retry-logica met exponential backoff geïmplementeerd
- Rate limiting om provider-throttling te voorkomen
Conclusie
E-mailinfrastructuur is niet glamoureus, maar het is een van de meest kritische systemen in je SaaS. Gebruikers verwachten dat transactionele e-mails direct en betrouwbaar aankomen. Als dat niet gebeurt, verdwijnt het vertrouwen snel.
Begin eenvoudig: kies een betrouwbare transactionele provider zoals Postmark of Resend, configureer je DNS correct en handel bounces vanaf dag één af. Naarmate je groeit, voeg je geautomatiseerde sequences en monitoring toe. De investering betaalt zich terug in minder supporttickets, betere activatiepercentages en hoger gebruikersvertrouwen.
Je e-mailinfrastructuur is het stille werkpaard van je SaaS. Behandel het met het respect dat het verdient.