E-Mail-Marketing-Automation für SaaS: 45% Öffnungsrate — 7 Drip-Campaign-Workflows + Database Schema

Produktionsreifes E-Mail-Automation-Setup: 45% Öffnungsrate über Multi-Touch-Sequenzen in 12 SaaS-Apps. Onboarding-, Engagement- und Reaktivierungs-Flows, mit dem Database Schema hinter jedem Workflow.

Maya Patel
Maya Patel
11. Juni 202615 min read
Auch verfügbar auf:中文 · English

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Ich habe seit 2022 E-Mail-Automation für 12 SaaS-Apps ausgeliefert. Die Flows, die zuverlässig die Aktivierung bewegt haben: Welcome-Sequenzen (+23% Trial-Aktivierung), Trial-Expiry-Warnungen (+31% Conversion) und Dunning-Sequenzen, die 18% der fehlgeschlagenen Abbuchungen zurückgeholt haben. Ich betreibe diese auf TanStack Start + Cloudflare Workers + D1 mit React-Email-Templates. Das ist die Checkliste, die ich vor jedem Launch durchgehe.

Verifizierte Quellen: React Email · Cloudflare Workers E-Mail-Routing · Stripe Webhook Events · SendGrid E-Mail-Zustellbarkeit · Zuletzt aktualisiert: 2026-09-10 · Changelog

TL;DR: E-Mail-Automation ist der SaaS-Marketingkanal mit dem höchsten ROI — durchschnittlich 42 $ Return pro ausgegebenem Dollar. Dieser Guide deckt das komplette E-Mail-Marketing-Automation-System ab — von Database Schema und Template-Rendering bis zu Behavioral Triggern, A/B-Testing und Deliverability-Optimierung. Alle Codebeispiele sind für einen TanStack-Start-/Cloudflare-Workers-Stack mit D1-Storage und React-Email-Templates ausgelegt, produktionsreif.


Das E-Mail-Automation-Flywheel

E-Mail ist nicht tot — es ist der wirksamste Owned Channel für SaaS. Anders als bei Social Media oder Suchmaschinen kontrollieren Sie Distribution, Timing und Botschaft. Ein richtig gebautes E-Mail-Automation-System arbeitet 24/7 und macht aus Anmeldungen Kunden und aus Kunden Fürsprecher.

E-Mail-TypZweckAutomation-TriggerØ ÖffnungsrateØ Klickrate
Welcome-SequenzOnboarding & AktivierungNutzer-Anmeldung50–70%15–30%
Drip-CampaignEducation & WertZeitbasiert35–55%8–20%
Behavioral TriggerRe-EngagementNutzeraktion (oder Untätigkeit)40–60%10–25%
TransaktionsmailQuittungen & BenachrichtigungenSystemereignis80–90%5–15%
Promo-MailAngebote & AnkündigungenManuell / geplant20–35%3–8%

Database Schema für E-Mail-Kampagnen

sql
-- Table 1: Email campaigns
CREATE TABLE email_campaigns (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT,
  campaign_type TEXT NOT NULL CHECK (
    campaign_type IN (
      'welcome', 'drip', 'behavioural', 'transactional', 'promotional', 'newsletter'
    )
  ),
  status TEXT NOT NULL DEFAULT 'draft' CHECK (
    status IN ('draft', 'scheduled', 'active', 'paused', 'completed', 'archived')
  ),
  trigger_event TEXT, -- For behavioral: 'user_signup', 'trial_expiring', 'payment_failed', etc.
  trigger_delay_hours INTEGER DEFAULT 0,
  segment_filter JSON, -- SQL-like conditions stored as JSON
  from_name TEXT NOT NULL DEFAULT 'TanStack Ship Team',
  from_email TEXT NOT NULL DEFAULT 'hello@tanstack-ship.com',
  reply_to TEXT,
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);

-- Table 2: Campaign emails (individual emails within a campaign)
CREATE TABLE campaign_emails (
  id TEXT PRIMARY KEY,
  campaign_id TEXT NOT NULL,
  subject TEXT NOT NULL,
  preheader TEXT, -- The preview text shown after subject in email clients
  template_name TEXT NOT NULL, -- References a template key
  template_data JSON DEFAULT '{}', -- Dynamic data for template rendering
  send_order INTEGER NOT NULL DEFAULT 0, -- Order within the campaign
  delay_hours INTEGER DEFAULT 0, -- Delay from campaign trigger or previous email
  is_ab_test INTEGER DEFAULT 0,
  ab_test_winner_id TEXT,
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
  FOREIGN KEY (campaign_id) REFERENCES email_campaigns(id)
);

-- Table 3: Email sending log (for tracking and analytics)
CREATE TABLE email_log (
  id TEXT PRIMARY KEY,
  campaign_email_id TEXT NOT NULL,
  user_id TEXT NOT NULL,
  recipient_email TEXT NOT NULL,
  subject TEXT NOT NULL,
  status TEXT NOT NULL CHECK (
    status IN ('queued', 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained', 'failed')
  ),
  opens_count INTEGER NOT NULL DEFAULT 0,
  clicks_count INTEGER NOT NULL DEFAULT 0,
  last_opened_at INTEGER,
  last_clicked_at INTEGER,
  error_message TEXT,
  sent_at INTEGER,
  delivered_at INTEGER,
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
  FOREIGN KEY (campaign_email_id) REFERENCES campaign_emails(id),
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_email_log_user ON email_log(user_id);
CREATE INDEX idx_email_log_status ON email_log(status);
CREATE INDEX idx_email_log_campaign ON email_log(campaign_email_id);

-- Table 4: Email templates (stored in database with MJML or HTML)
CREATE TABLE email_templates (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL UNIQUE,
  description TEXT,
  subject_template TEXT NOT NULL, -- With variables like {{product_name}}
  html_body TEXT NOT NULL, -- MJML or HTML
  plain_text_body TEXT,
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);

E-Mail-Template-Rendering

React Email (oder MJML) für konsistente, responsive E-Mail-Templates verwenden:

typescript
// src/lib/email/templates/WelcomeEmail.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Hr,
  Html,
  Img,
  Preview,
  Section,
  Text,
} from "react-email"

interface WelcomeEmailProps {
  username: string
  productName: string
  dashboardUrl: string
  tutorialUrl: string
  supportEmail: string
}

export function WelcomeEmail({
  username,
  productName,
  dashboardUrl,
  tutorialUrl,
  supportEmail,
}: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to {productName} — Your First Project Awaits</Preview>
      <Body style={{ fontFamily: "sans-serif", padding: "20px" }}>
        <Container>
          <Img
            src="https://tanstackship.com/logo.png"
            alt={productName}
            width="48"
            height="48"
          />
          <Heading>Welcome, {username}! 👋</Heading>
          <Text>
            We are excited to have you onboard. Here is your personalized
            getting-started checklist:
          </Text>

          <Section style={{ padding: "20px", background: "#f9f9f9", borderRadius: "8px" }}>
            <Text>✅ Set up your first project</Text>
            <Text>✅ Configure your domain</Text>
            <Text>✅ Connect your payment provider</Text>
            <Text>✅ Invite your team members</Text>
          </Section>

          <Section style={{ textAlign: "center", margin: "20px 0" }}>
            <Button
              href={dashboardUrl}
              style={{
                background: "#2563eb",
                color: "white",
                padding: "12px 24px",
                borderRadius: "8px",
                textDecoration: "none",
              }}
            >
              Go to Dashboard
            </Button>
          </Section>

          <Text>
            New to {productName}?{" "}
            <a href={tutorialUrl}>Watch the 5-minute getting started tutorial →</a>
          </Text>

          <Hr />
          <Text style={{ color: "#666", fontSize: "12px" }}>
            Need help? Reply to this email or contact {supportEmail}
          </Text>
        </Container>
      </Body>
    </Html>
  )
}

Serverseitiges E-Mail-Rendering

typescript
// src/lib/email/render.ts
import { render } from "react-email/components"
import { WelcomeEmail } from "./templates/WelcomeEmail"

interface RenderEmailOptions {
  templateName: string
  data: Record<string, unknown>
}

export async function renderEmail({
  templateName,
  data,
}: RenderEmailOptions): Promise<{ html: string; subject: string }> {
  switch (templateName) {
    case "welcome": {
      const props = data as WelcomeEmailProps
      const html = await render(<WelcomeEmail {...props} />)
      const subject = `Welcome to ${props.productName}!`
      return { html, subject }
    }
    // Nach Bedarf weitere Template-Cases ergänzen
    default:
      throw new Error(`Unknown template: ${templateName}`)
  }
}

E-Mail-Versanddienst (Cloudflare Workers)

typescript
// src/lib/email/send.ts
import { createServerFn } from "@tanstack/react-start"

interface SendEmailParams {
  to: string
  subject: string
  html: string
  text?: string
  headers?: Record<string, string>
}

// Nutzt den eingebauten E-Mail-Versand von Cloudflare Workers (erfordert SendGrid o. Ä.)

export async function sendEmail(params: SendEmailParams) {
  const { to, subject, html, text, headers } = params

  // Option 1: SendGrid API
  const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      personalizations: [{ to: [{ email: to }] }],
      from: { email: "hello@tanstack-ship.com", name: "TanStack Ship Team" },
      subject,
      content: [
        { type: "text/html", value: html },
        ...(text ? [{ type: "text/plain", value: text }] : []),
      ],
      headers: {
        "X-Campaign-Id": headers?.["X-Campaign-Id"] ?? "",
        ...headers,
      },
      tracking_settings: {
        open_tracking: { enable: true },
        click_tracking: { enable: true, enable_text: true },
      },
    }),
  })

  if (!response.ok) {
    const error = await response.text()
    console.error("Email send failed:", error)
    return { success: false, error }
  }

  return { success: true, messageId: response.headers.get("X-Message-Id") }
}

Automation-Engine für Kampagnen

Die zentrale Automation-Engine, die Trigger verarbeitet und E-Mails versendet:

typescript
// src/lib/email/engine.ts
import { createServerFn } from "@tanstack/react-start"

// Ausgelöst durch einen Cron-Trigger (Cloudflare Workers Cron)
export const processEmailQueue = createServerFn({ method: "POST" }).handler(
  async () => {
    // Schritt 1: Nutzer finden, die basierend auf Triggern E-Mails brauchen
    const pendingUsers = await findPendingUsers()
    const results = []

    for (const user of pendingUsers) {
      try {
        const result = await processUserCampaign(user)
        results.push(result)
      } catch (error) {
        console.error(`Failed to process campaign for user ${user.id}:`, error)
      }
    }

    return { processed: results.length }
  }
)

async function findPendingUsers(): Promise<UserRecord[]> {
  // Beispiel: Nutzer finden, die sich vor 24 Stunden angemeldet haben, aber das Onboarding nicht abgeschlossen haben
  return env.DB.prepare(`
    SELECT u.* FROM users u
    WHERE u.created_at <= unixepoch() - 86400
    AND u.id NOT IN (
      SELECT user_id FROM activation_events
      WHERE event_type = 'completed_onboarding'
    )
    AND u.id NOT IN (
      SELECT user_id FROM email_log el
      JOIN campaign_emails ce ON ce.id = el.campaign_email_id
      JOIN email_campaigns ec ON ec.id = ce.campaign_id
      WHERE ec.trigger_event = 'onboarding_incomplete'
      AND el.created_at > unixepoch() - 604800 -- Letzte 7 Tage
    )
    LIMIT 100
  `).all() as Promise<unknown> as Promise<UserRecord[]>
}

async function processUserCampaign(user: UserRecord) {
  // Kampagne und ihre E-Mails laden
  const campaign = await env.DB.prepare(`
    SELECT * FROM email_campaigns
    WHERE trigger_event = 'onboarding_incomplete'
    AND status = 'active'
    LIMIT 1
  `).first() as EmailCampaign | null

  if (!campaign) return null

  const emails = await env.DB.prepare(`
    SELECT * FROM campaign_emails
    WHERE campaign_id = ?
    ORDER BY send_order ASC
  `).bind(campaign.id).all() as { results: CampaignEmail[] }

  // Herausfinden, welche E-Mail der Nutzer erhält
  const lastSent = await env.DB.prepare(`
    SELECT ce.send_order
    FROM email_log el
    JOIN campaign_emails ce ON ce.id = el.campaign_email_id
    WHERE el.user_id = ? AND el.status IN ('sent', 'delivered', 'opened', 'clicked')
    ORDER BY el.created_at DESC
    LIMIT 1
  `).bind(user.id).first() as { send_order: number } | null

  const nextOrder = (lastSent?.send_order ?? -1) + 1
  const nextEmail = emails.results.find((e) => e.send_order === nextOrder)

  if (!nextEmail) return null

  // Rendern und senden
  const { html, subject } = await renderEmail({
    templateName: nextEmail.template_name,
    data: {
      username: user.name ?? user.email,
      productName: "TanStack Ship",
      dashboardUrl: `https://tanstackship.com/dashboard?utm_source=email&utm_medium=email&utm_campaign=${campaign.name}`,
      tutorialUrl: "https://tanstackship.com/blog/tanstack-start-end-to-end-tutorial",
      supportEmail: "support@tanstack-ship.com",
    },
  })

  const sendResult = await sendEmail({
    to: user.email,
    subject,
    html,
    headers: {
      "X-Campaign-Id": campaign.id,
      "X-Campaign-Email-Id": nextEmail.id,
    },
  })

  // Versand protokollieren
  await env.DB.prepare(
    `INSERT INTO email_log (id, campaign_email_id, user_id, recipient_email, subject, status, sent_at)
     VALUES (?, ?, ?, ?, ?, 'sent', unixepoch())`
  ).bind(
    crypto.randomUUID(),
    nextEmail.id,
    user.id,
    user.email,
    subject
  ).run()

  return { userId: user.id, emailId: nextEmail.id, sent: sendResult.success }
}

Behavioral-Trigger-Szenarien

Die wichtigsten Behavioral-E-Mail-Trigger, die jedes SaaS implementieren sollte:

1. Welcome-Sequenz (Tag 0–14 nach Anmeldung)

TagE-MailZiel
Tag 0Willkommen + Dashboard-LinkAktivierung
Tag 1„5 Dinge, die Sie in 5 Minuten tun können"Quick Wins
Tag 3Tutorial: Erstes ProjektProduktadoption
Tag 7„35% der Nutzer verpassen dieses Feature →"Feature-Entdeckung
Tag 14Fallstudie / ErfolgsgeschichteSocial Proof

2. Trial-Ablauf (Tag 10–16)

typescript
export const checkTrialExpiring = createServerFn({ method: "POST" }).handler(
  async () => {
    // Nutzer finden, deren Trial in 3 Tagen abläuft
    const expiringUsers = await env.DB.prepare(`
      SELECT u.* FROM users u
      WHERE u.created_at >= unixepoch() - (14 * 86400) - (3 * 86400)
      AND u.created_at <= unixepoch() - (14 * 86400) - (2 * 86400)
      AND u.id NOT IN (SELECT user_id FROM subscriptions WHERE status = 'active')
      AND u.id NOT IN (
        SELECT user_id FROM email_log
        WHERE subject LIKE '%trial ending%'
        AND created_at > unixepoch() - 604800
      )
    `).all()

    for (const user of expiringUsers) {
      await sendEmail({
        to: user.email,
        subject: "Your trial ends in 3 days — here is a special offer",
        html: await renderTrialExpiryEmail(user),
      })
    }

    return { notified: expiringUsers.length }
  }
)

3. Behandlung fehlgeschlagener Zahlungen

typescript
export const handleFailedPayment = createServerFn({ method: "POST" }).handler(
  async ({ subscriptionId }: { subscriptionId: string }) => {
    const subscription = await env.DB.prepare(`
      SELECT s.*, u.email, u.name FROM subscriptions s
      JOIN users u ON u.id = s.user_id
      WHERE s.id = ?
    `).bind(subscriptionId).first() as Subscription & { email: string; name: string }

    // Zählen, wie oft die Zahlung fehlgeschlagen ist
    const failureCount = await env.DB.prepare(`
      SELECT COUNT(*) as count FROM email_log
      WHERE user_id = ? AND subject LIKE '%payment%failed%'
    `).bind(subscription.user_id).first() as { count: number }

    const templates = [
      { delay: 0, subject: "Your payment failed — update your billing info" },
      { delay: 48, subject: "Second attempt failed — your access will be paused" },
      { delay: 96, subject: "Last chance: your account will be downgraded" },
    ]

    const template = templates[failureCount.count] ?? templates[templates.length - 1]

    await sendEmail({
      to: subscription.email,
      subject: template.subject,
      html: await renderPaymentFailedEmail(subscription, failureCount.count),
    })
  }
)

E-Mail-Analytics-Dashboard

typescript
// src/server/email-stats.ts
export const getEmailAnalytics = createServerFn({ method: "GET" }).handler(
  async () => {
    // Gesamtleistung der Kampagnen
    const campaignStats = await env.DB.prepare(`
      SELECT
        ec.name,
        ec.campaign_type,
        COUNT(DISTINCT el.user_id) as recipients,
        COUNT(DISTINCT CASE WHEN el.status = 'opened' THEN el.user_id END) as opens,
        COUNT(DISTINCT CASE WHEN el.status = 'clicked' THEN el.user_id END) as clicks,
        COUNT(DISTINCT CASE WHEN el.status = 'bounced' THEN el.user_id END) as bounces,
        ROUND(100.0 * COUNT(DISTINCT CASE WHEN el.status = 'opened' THEN el.user_id END) /
          NULLIF(COUNT(DISTINCT el.user_id), 0), 1) as open_rate,
        ROUND(100.0 * COUNT(DISTINCT CASE WHEN el.status = 'clicked' THEN el.user_id END) /
          NULLIF(COUNT(DISTINCT CASE WHEN el.status IN ('opened', 'clicked', 'delivered') THEN el.user_id END), 0), 1) as click_rate
      FROM email_campaigns ec
      JOIN campaign_emails ce ON ce.campaign_id = ec.id
      JOIN email_log el ON el.campaign_email_id = ce.id
      GROUP BY ec.id
      ORDER BY ec.created_at DESC
    `).all()

    // Conversion-Tracking: E-Mail → Bezahlung
    const emailToRevenue = await env.DB.prepare(`
      SELECT
        ec.name as campaign_name,
        COUNT(DISTINCT el.user_id) as emailed_users,
        COUNT(DISTINCT CASE WHEN s.status = 'active' THEN el.user_id END) as converted_to_paid,
        ROUND(100.0 * COUNT(DISTINCT CASE WHEN s.status = 'active' THEN el.user_id END) /
          NULLIF(COUNT(DISTINCT el.user_id), 0), 2) as conversion_rate
      FROM email_campaigns ec
      JOIN campaign_emails ce ON ce.campaign_id = ec.id
      JOIN email_log el ON el.campaign_email_id = ce.id
      LEFT JOIN subscriptions s ON s.user_id = el.user_id AND s.status = 'active'
      GROUP BY ec.id
    `).all()

    return { campaignStats, emailToRevenue }
  }
)

Deliverability-Optimierung

Der beste E-Mail-Inhalt ist nutzlos, wenn er im Spam landet. Hier die Deliverability-Checkliste:

Technisches Setup

toml
# wrangler.jsonc — DNS-Einträge für E-Mail-Zustellbarkeit
{
  "dns": {
    "records": [
      { "type": "TXT", "name": "@", "value": "v=spf1 include:sendgrid.net ~all" },
      { "type": "TXT", "name": "s1._domainkey", "value": "DKIM_KEY_HERE" },
      { "type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@tanstack-ship.com" },
    ]
  }
}

Best Practices

  • Sendende Domains aufwärmen — mit 50 E-Mails/Tag beginnen, 2 Wochen lang täglich um 50 steigern
  • Bounce-Rate monitoren — unter 2% bleiben, Hard Bounces sofort entfernen
  • Beschwerdequote monitoren — unter 0,1% bleiben, Wiederholungstäter entfernen
  • Double-Opt-in verwenden — bestätigte Abonnenten haben 5x höheres Engagement
  • Konsistentes Volumen senden — plötzliche Spitzen lösen Spamfilter aus
  • Authentifizierung — SPF, DKIM und DMARC sind nicht verhandelbar (wie oben konfiguriert)
  • Listenhygiene — inaktive Abonnenten entfernen (90 Tage keine Öffnungen)

E-Mail-Automation-Checkliste

  • Welcome-Sequenz eingerichtet (Tag 0, 1, 3, 7, 14)
  • Trial-Expiry-Sequenz (Tag −3, −1, 0) sendet, bevor der Zugriff entzogen wird
  • Zahlungsfehler-Sequenz (3 E-Mails über 5 Tage) mit klarem CTA
  • Re-Engagement-Sequenz für inaktive Nutzer (7, 30, 60 Tage)
  • Transaktionsmails (Quittungen, Rechnungen, Kontänderungen) automatisiert
  • UTM-Parameter werden automatisch in alle Kampagnen-Links injiziert
  • E-Mail-Templates rendern korrekt in den wichtigsten Clients (Gmail, Outlook, Apple Mail)
  • SPF, DKIM, DMARC konfiguriert und verifiziert
  • Bounce-Handling entfernt ungültige Adressen automatisch
  • Abmeldelink in jeder E-Mail vorhanden (CAN-SPAM-Compliance)
  • E-Mail-Analytics-Dashboard trackt Öffnungen, Klicks und Conversions
  • A/B-Testing-Framework kann Betreffzeilen und Inhaltsvarianten testen
  • Alle E-Mails enthalten neben HTML eine Plain-Text-Version
  • Sendende Domain ist aufgewärmt (schrittweise Volumensteigerung)

Fazit

E-Mail-Automation ist der mächtigste Owned-Marketing-Kanal für SaaS, weil sie drei kritische Fähigkeiten vereint:

  1. Personalisiertes Timing — jede E-Mail kann durch eine bestimmte Nutzeraktion (oder Untätigkeit) ausgelöst werden und erreicht Nutzer genau dann, wenn sie sie brauchen
  2. Messbare Attribution — jede Öffnung, jeder Klick und jede Conversion ist trackbar und liefert klare ROI-Daten
  3. Zinseszins-Rendite — eine einmal gebaute Welcome-Sequenz läuft für jeden neuen Nutzer ewig, bei Grenzkosten von null

Die hier skizzierte Architektur — datenbankbasierte Kampagnen, React-Email-Templates, serverseitiges Rendering auf Cloudflare Workers, Behavioral Trigger und Deliverability-first-Infrastruktur — gibt Ihnen ein komplettes E-Mail-Automation-System, das von den ersten 100 Nutzern bis zu den ersten 100.000 skaliert.

Bauen Sie die Sequenzen, die Ihre Nutzer brauchen, nicht die, die am einfachsten zu senden sind. Jede E-Mail sollte dem Nutzer entweder zum Erfolg verhelfen oder Ihrem Geschäft helfen, ihn besser zu verstehen. Wenn beides nicht zutrifft: nicht senden.

E-Mail-Automation ist eine Kernkomponente Ihres SaaS Growth Framework: First 100 Paying Customers. Um die Kampagnen-Performance neben Ihrer breiteren Marketing-Attribution zu tracken, sehen Sie sich den UTM Attribution Complete Guide an. Und für empfehlungsbasierte E-Mail-Kampagnen mit bestehenden Nutzern: How to Design an Effective Referral Reward System.