E-Mail-Dienst

Transaktionale E-Mails mit Resend und React Email — 6 Vorlagen, Datenbank-Logging und Webhook-Tracking

Das Kit enthält ein vollständiges transaktionales E-Mail-System, das auf Resend und React Email basiert. Das System umfasst 6 vorgefertigte Vorlagen, automatische Wiederholungsversuche mit exponentiellem Backoff, Datenbank-Logging mit Statusverfolgung sowie Webhook-Verarbeitung für Zustellungsanalysen.
Diese Seite behandelt die Einrichtung, das Versenden von E-Mails, Vorlagen und die Webhook-Verarbeitung. Informationen zur Konfiguration des Rate Limitings findest du unter Caching & Redis. Für zahlungsbezogene E-Mails siehe Webhooks & Kundenportal.

Funktionsweise

Jede E-Mail durchläuft eine dreistufige Pipeline — von der Service-Funktion zur Resend-API und zurück über Webhooks:
Service Function (sendWelcomeEmail, sendContactConfirmation, ...)
    |
    |--- 1. Rate-Limit-Prüfung (wurde diese E-Mail kürzlich gesendet?)
    |--- 2. React-Email-Vorlage zu HTML rendern
    |--- 3. Versuchsprotokoll in Datenbank speichern (Status: PENDING)
    |
    v
E-Mail-Client (client.ts)
    |--- Sendet über Resend SDK
    |--- Wiederholt bei Fehler (3 Versuche, exponentieller Backoff)
    |--- Überspringt Wiederholung bei Validierungsfehlern (4xx)
    |
    v
Resend API
    |--- Zustellung ins Postfach des Empfängers
    |--- Sendet Webhook-Events (delivered, opened, clicked, bounced)
    |
    v
Webhook-Route (/api/webhooks/resend)
    |--- Verifiziert HMAC-SHA256-Signatur
    |--- Aktualisiert EmailLog-Status in der Datenbank
    |--- Verfolgt: DELIVERED → OPENED → CLICKED oder BOUNCED

Einrichtung

1

Resend-API-Schlüssel erstellen

Erstelle ein Konto auf resend.com und generiere einen API-Schlüssel auf der Seite API Keys in deinem Dashboard. Der kostenlose Plan umfasst 3.000 E-Mails/Monat.
2

Umgebungsvariablen konfigurieren

Füge Folgendes zu deiner apps/boilerplate/.env.local hinzu:
bash
RESEND_API_KEY=re_your_api_key_here
RESEND_FROM_EMAIL=noreply@yourdomain.com
RESEND_WEBHOOK_SECRET=whsec_your_webhook_secret  # Optional, empfohlen für Produktion
3

Domain verifizieren

Gehe im Resend-Dashboard zu Domains und füge deine Absender-Domain hinzu. Folge den DNS-Verifikationsschritten (füge die erforderlichen TXT-, MX- und DKIM-Einträge hinzu). Bis zur Verifizierung werden E-Mails von Resends geteilter Domain gesendet.

E-Mails versenden

Der E-Mail-Dienst stellt dedizierte Funktionen für jeden E-Mail-Typ bereit. Jede Funktion übernimmt Rate Limiting, Vorlagen-Rendering, Datenbank-Logging und Fehlerbehandlung automatisch:
src/lib/email/service.ts — sendWelcomeEmail
export async function sendWelcomeEmail(
  userId: string,
  email: string,
  name?: string
): Promise<boolean> {
  try {
    // Check rate limiting
    const recentlySent = await wasEmailRecentlySent(
      email,
      EmailType.WELCOME,
      60 * 24 // 24 hours
    )

    if (recentlySent) {
      console.log(`[Email] Welcome email recently sent to ${email}, skipping`)
      return false
    }

    // Render email template
    const html = await render(
      WelcomeEmail({
        name: name || 'there',
        email,
        dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
      })
    )

    // Log attempt
    const emailLog = await logEmail({
      user: { connect: { id: userId } },
      to: email,
      from: process.env.RESEND_FROM_EMAIL,
      subject: 'Welcome to Our Platform!',
      type: EmailType.WELCOME,
      status: EmailStatus.PENDING,
      metadata: { templateUsed: 'welcome' },
    })

    // Send email
    const result = await sendEmail({
      to: formatEmailAddress(email, name),
      subject: 'Welcome to Our Platform!',
      html,
    })
Die zugrunde liegende sendEmail-Funktion in client.ts behandelt Wiederholungsversuche mit exponentiellem Backoff. Bei Serverfehlern (5xx) wird automatisch ein neuer Versuch unternommen, bei Validierungsfehlern (4xx) jedoch nicht:
src/lib/email/client.ts — sendEmail with Retry Logic
export async function sendEmail(options: EmailOptions): Promise<EmailResult> {
  const {
    from = DEFAULT_FROM_EMAIL,
    retries = 3,
    retryDelay = 1000,
    ...emailOptions
  } = options

  let lastError: Error | null = null
  let attempt = 0

  while (attempt < retries) {
    try {
      const response = await resend.emails.send({
        from,
        ...emailOptions,
      } as CreateEmailOptions)

      // Check for Resend API errors
      if ('error' in response && response.error) {
        throw new Error(response.error.message || 'Unknown Resend error')
      }

      return {
        success: true,
        data: response as CreateEmailResponse,
      }
    } catch (error) {
      lastError = error as Error
      attempt++

      // Don't retry on validation errors (4xx)
      if (isValidationError(error)) {
        break
      }

      // Wait before retrying (exponential backoff)
      if (attempt < retries) {
        await delay(retryDelay * Math.pow(2, attempt - 1))
      }
    }
  }

  // All retries failed
  const errorMessage = lastError?.message || 'Failed to send email'
  console.error('[Email] Send failed after retries:', errorMessage)

  return {
    success: false,
    error: errorMessage,
  }
}

Verfügbare Service-Funktionen

FunktionZweckRate Limit
sendWelcomeEmail()Neue Benutzerregistrierung1 pro 24 Stunden pro E-Mail
sendContactConfirmation()Kontaktformular-Übermittlung1 pro 5 Minuten pro E-Mail
sendContactNotificationToAdmin()Admin-Benachrichtigung bei neuem KontaktKeine Duplikat-Prüfung
sendSubscriptionEmail()Abonnement-Lifecycle-Events1 pro 30 Minuten pro E-Mail
sendTemplatedEmail()DUAL-Pricing-E-Mails (Trial, Credits)1 pro 30 Minuten pro E-Mail
sendTestEmail()Entwicklung/TestsKein Rate Limit

E-Mail-Vorlagen

Das Kit enthält 6 React-Email-Vorlagen, die mit gemeinsamen Komponenten (Header, Footer, Button) erstellt wurden:
VorlageDateiProps
Willkommenwelcome.tsxname, email, dashboardUrl
Kontaktbestätigungcontact-confirmation.tsxname, email, message
Abonnement gekündigtsubscription-cancelled.tsxname, planName, endDate
Trial abgelaufen (Free)trial-expired-free.tsxname, freeTierFeatures, upgradeUrl
Trial abgelaufen (gesperrt)trial-expired-locked.tsxname, pricingUrl
Bonus-Credits gekauftbonus-credits-purchased.tsxname, credits, price, packageName, newBalance
Hier ist die Willkommens-Vorlage als Beispiel:
src/emails/templates/welcome.tsx
import { Heading, Section, Text } from '@react-email/components'
import { Button } from '../components/button'
import { EmailFooter } from '../components/footer'
import { EmailHeader } from '../components/header'

interface WelcomeEmailProps {
  name?: string
  email: string
  dashboardUrl?: string
}

export default function WelcomeEmail({
  name = 'there',
  email,
  dashboardUrl = 'https://example.com/dashboard',
}: WelcomeEmailProps) {
  const preview = `Welcome to our platform, ${name}!`

  return (
    <EmailHeader preview={preview}>
      <Section style={box}>
        <Heading style={heading}>Welcome aboard! 🎉</Heading>
        <Text style={paragraph}>Hi {name},</Text>
        <Text style={paragraph}>
          We&apos;re thrilled to have you join our community! Your account has
          been successfully created with the email address:{' '}
          <strong>{email}</strong>
        </Text>
        <Text style={paragraph}>Here&apos;s what you can do next:</Text>
        <Section style={list}>
          <Text style={listItem}>✓ Complete your profile</Text>
          <Text style={listItem}>✓ Explore our features</Text>
          <Text style={listItem}>✓ Connect with other users</Text>
          <Text style={listItem}>✓ Customize your settings</Text>
        </Section>
        <Section style={buttonContainer}>
          <Button href={dashboardUrl}>Go to Dashboard</Button>
        </Section>
        <Text style={paragraph}>
          If you have any questions or need help getting started, our support
          team is here to help. Simply reply to this email or visit our help
          center.
        </Text>
        <Text style={paragraph}>
          Best regards,
          <br />
          The Team
        </Text>
      </Section>
      <EmailFooter />
    </EmailHeader>
  )
}

Gemeinsame Komponenten

Alle Vorlagen verwenden drei gemeinsame Komponenten aus apps/boilerplate/src/emails/components/:
  • EmailHeader — Bettet die E-Mail in ein einheitliches Layout mit Logo und Vorschautext ein
  • EmailFooter — Fügt Abmelde-Link, Unternehmensadresse und rechtliche Hinweise hinzu
  • Button — Gestalteter CTA-Button, der konsistent in allen E-Mail-Clients gerendert wird

Eigene Vorlagen erstellen

1

Vorlagendatei erstellen

Füge eine neue Datei in apps/boilerplate/src/emails/templates/ hinzu. Verwende die vorhandenen Vorlagen als Referenz:
tsx
// src/emails/templates/order-confirmation.tsx
import { Heading, Section, Text } from '@react-email/components'
import { Button } from '../components/button'
import { EmailFooter } from '../components/footer'
import { EmailHeader } from '../components/header'

interface OrderConfirmationProps {
  name: string
  orderId: string
  amount: string
}

export default function OrderConfirmation({
  name = 'Customer',
  orderId,
  amount,
}: OrderConfirmationProps) {
  return (
    <EmailHeader preview={`Order ${orderId} confirmed`}>
      <Section style={{ padding: '0 48px' }}>
        <Heading>Order Confirmed</Heading>
        <Text>Hi {name}, your order #{orderId} for {amount} has been confirmed.</Text>
        <Button href={`${process.env.NEXT_PUBLIC_APP_URL}/orders/${orderId}`}>
          View Order
        </Button>
      </Section>
      <EmailFooter />
    </EmailHeader>
  )
}
2

Service-Funktion hinzufügen

Füge eine neue Funktion in apps/boilerplate/src/lib/email/service.ts hinzu, die die Vorlage rendert und sendet:
typescript
export async function sendOrderConfirmation(
  email: string,
  data: { name: string; orderId: string; amount: string }
): Promise<boolean> {
  const html = await render(
    OrderConfirmation({ name: data.name, orderId: data.orderId, amount: data.amount })
  )

  const result = await sendEmail({
    to: formatEmailAddress(email, data.name),
    subject: `Order ${data.orderId} Confirmed`,
    html,
  })

  return result.success
}
3

Vorlage in der Vorschau anzeigen

Starte den React-Email-Entwicklungsserver, um Vorlagen im Browser zu betrachten:
bash
pnpm email:dev
Dies startet einen lokalen Server, auf dem du alle Vorlagen mit Live-Reload anzeigen und testen kannst.

Kontaktformular-Integration

Das Kit enthält einen vollständigen Kontaktformular-Ablauf, der automatisch zwei E-Mails sendet:
  1. Bestätigung an den AbsendersendContactConfirmation() sendet eine „Wir haben deine Nachricht erhalten"-E-Mail
  2. Benachrichtigung an den AdminsendContactNotificationToAdmin() benachrichtigt die konfigurierte Admin-E-Mail-Adresse
Die Admin-E-Mail wird über NEXT_PUBLIC_CONTACT_EMAIL bestimmt (Fallback auf RESEND_FROM_EMAIL). Die Kontaktformular-API-Route unter /api/contact löst beide E-Mails aus, nachdem die Eingabe validiert und bereinigt wurde.

Webhook-Verarbeitung

Resend sendet Webhook-Events, wenn sich der E-Mail-Status ändert. Das Kit verarbeitet diese automatisch, um den EmailLog-Status in der Datenbank zu aktualisieren:
src/app/api/webhooks/resend/route.ts — Signature Verification
import { NextRequest, NextResponse } from 'next/server'
import { headers } from 'next/headers'
import crypto from 'crypto'
import { processEmailWebhook } from '@/lib/email/service'
import { withRateLimit } from '@/lib/security/rate-limit-middleware'

/**
 * Verify webhook signature from Resend
 */
function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  try {
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex')

    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    )
  } catch (error) {
    console.error('[Webhook] Signature verification error:', error)
    return false
  }
}

export const POST = withRateLimit('webhooks', async (request: NextRequest) => {
  try {
    // Get raw body for signature verification
    const rawBody = await request.text()

    // Get signature from headers
    const headersList = await headers()
    const signature = headersList.get('resend-signature')

    // Verify webhook signature if secret is configured
    const webhookSecret = process.env.RESEND_WEBHOOK_SECRET
    if (webhookSecret && signature) {
      const isValid = verifyWebhookSignature(rawBody, signature, webhookSecret)
      if (!isValid) {
        console.warn('[Webhook] Invalid signature')
        return NextResponse.json(
          { error: 'Invalid signature' },
          { status: 401 }
        )
      }
    } else if (process.env.NODE_ENV === 'production') {
      // In production, webhook secret should always be configured
      console.error('[Webhook] No webhook secret configured')
      return NextResponse.json(
        { error: 'Webhook not configured' },
        { status: 500 }
      )
    }

Verfolgte Events

Resend EventDatenbank-StatusBeschreibung
email.deliveredDELIVEREDE-Mail hat das Postfach des Empfängers erreicht
email.openedOPENEDEmpfänger hat die E-Mail geöffnet
email.clickedCLICKEDEmpfänger hat einen Link angeklickt
email.bouncedBOUNCEDE-Mail hat gebounced (ungültige Adresse)
email.complainedFAILEDEmpfänger hat als Spam markiert

Datenbank-Logging

Jede E-Mail, die über den Dienst gesendet wird, wird im EmailLog-Modell mit vollständiger Lifecycle-Verfolgung protokolliert:
EmailLog
├── id           # Automatisch generierte UUID
├── userId       # Verknüpft mit User (optional für System-E-Mails)
├── to           # Empfänger-E-Mail
├── from         # Absender-E-Mail
├── subject      # E-Mail-Betreff
├── type         # WELCOME | TRANSACTION | NOTIFICATION | SYSTEM | ...
├── status       # PENDING → SENT → DELIVERED → OPENED → CLICKED
├── messageId    # Resend-Nachrichten-ID (für Webhook-Korrelation)
├── metadata     # JSON — verwendete Vorlage, Webhook-Daten, Fehlerdetails
├── createdAt    # Zeitpunkt der E-Mail-Einreihung
└── updatedAt    # Letzte Statusänderung
Der Status durchläuft einen Lifecycle: PENDINGSENTDELIVEREDOPENEDCLICKED. Bei fehlgeschlagener Zustellung wechselt der Status stattdessen zu FAILED oder BOUNCED.

Rate Limiting

Jeder E-Mail-Typ hat ein integriertes Rate Limit, um doppelte Sendungen zu verhindern. Die Funktion wasEmailRecentlySent() prüft die EmailLog-Tabelle vor dem Senden:
E-Mail-TypZeitfensterAuswirkung
Willkommen24 StundenVerhindert doppelte Willkommens-E-Mails bei Neuregistrierung
Kontaktbestätigung5 MinutenVerhindert Kontaktformular-Spam
Abonnement-Events30 MinutenVerhindert doppelte Abonnement-Benachrichtigungen
Vorlagen-E-Mails30 MinutenVerhindert doppelte Credit-/Trial-E-Mails
Test-E-MailKeineWird immer gesendet (nur in der Entwicklung)

Umgebungsvariablen

VariableErforderlichStandardZweck
RESEND_API_KEYJatest_keyResend-API-Schlüssel zum Senden von E-Mails
RESEND_FROM_EMAILJanoreply@example.comStandard-Absender-E-Mail-Adresse
RESEND_WEBHOOK_SECRETNeinHMAC-Secret zur Webhook-Signaturverifizierung
NEXT_PUBLIC_CONTACT_EMAILNeinFallback auf RESEND_FROM_EMAILAdmin-E-Mail für Kontaktformular-Benachrichtigungen
NEXT_PUBLIC_APP_URLJaBasis-URL für Links in E-Mail-Vorlagen

Wichtige Dateien

DateiZweck
apps/boilerplate/src/lib/email/service.tsÜbergeordnete E-Mail-Funktionen (sendWelcomeEmail, sendContactConfirmation usw.)
apps/boilerplate/src/lib/email/client.tsResend-SDK-Client mit Retry-Logik und exponentiellem Backoff
apps/boilerplate/src/lib/email/types.tsTypeScript-Typen für E-Mail-Optionen, Webhook-Daten
apps/boilerplate/src/lib/db/queries/email-logs.tsDatenbankabfragen für EmailLog (Logging, Statusaktualisierung, Duplikat-Prüfung)
apps/boilerplate/src/emails/templates/6 React-Email-Vorlagen
apps/boilerplate/src/emails/components/Gemeinsame E-Mail-Komponenten (Header, Footer, Button)
apps/boilerplate/src/app/api/webhooks/resend/route.tsWebhook-Endpunkt mit Signaturverifizierung