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.
Während der Entwicklung erlaubt Resend das Versenden an deine eigene E-Mail-Adresse ohne Domain-Verifizierung. Eine verifizierte Domain wird nur benötigt, um in der Produktion E-Mails an andere Empfänger zu senden.
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
| Funktion | Zweck | Rate Limit |
|---|---|---|
sendWelcomeEmail() | Neue Benutzerregistrierung | 1 pro 24 Stunden pro E-Mail |
sendContactConfirmation() | Kontaktformular-Übermittlung | 1 pro 5 Minuten pro E-Mail |
sendContactNotificationToAdmin() | Admin-Benachrichtigung bei neuem Kontakt | Keine Duplikat-Prüfung |
sendSubscriptionEmail() | Abonnement-Lifecycle-Events | 1 pro 30 Minuten pro E-Mail |
sendTemplatedEmail() | DUAL-Pricing-E-Mails (Trial, Credits) | 1 pro 30 Minuten pro E-Mail |
sendTestEmail() | Entwicklung/Tests | Kein Rate Limit |
E-Mail-Vorlagen
Das Kit enthält 6 React-Email-Vorlagen, die mit gemeinsamen Komponenten (Header, Footer, Button) erstellt wurden:
| Vorlage | Datei | Props |
|---|---|---|
| Willkommen | welcome.tsx | name, email, dashboardUrl |
| Kontaktbestätigung | contact-confirmation.tsx | name, email, message |
| Abonnement gekündigt | subscription-cancelled.tsx | name, planName, endDate |
| Trial abgelaufen (Free) | trial-expired-free.tsx | name, freeTierFeatures, upgradeUrl |
| Trial abgelaufen (gesperrt) | trial-expired-locked.tsx | name, pricingUrl |
| Bonus-Credits gekauft | bonus-credits-purchased.tsx | name, 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'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'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 einEmailFooter— Fügt Abmelde-Link, Unternehmensadresse und rechtliche Hinweise hinzuButton— 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:
- Bestätigung an den Absender —
sendContactConfirmation()sendet eine „Wir haben deine Nachricht erhalten"-E-Mail - Benachrichtigung an den Admin —
sendContactNotificationToAdmin()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 Event | Datenbank-Status | Beschreibung |
|---|---|---|
email.delivered | DELIVERED | E-Mail hat das Postfach des Empfängers erreicht |
email.opened | OPENED | Empfänger hat die E-Mail geöffnet |
email.clicked | CLICKED | Empfänger hat einen Link angeklickt |
email.bounced | BOUNCED | E-Mail hat gebounced (ungültige Adresse) |
email.complained | FAILED | Empfänger hat als Spam markiert |
Um Webhook-Events zu empfangen, füge deine Webhook-URL im Resend-Dashboard unter Webhooks hinzu:
https://yourdomain.com/api/webhooks/resend. Wähle die Events aus, die du verfolgen möchtest, und kopiere das Signing-Secret in RESEND_WEBHOOK_SECRET.Der Resend-Webhook-Handler des Kits gibt
200 OK zurück, auch wenn die Verarbeitung intern fehlschlägt. Das ist beabsichtigt — das Zurückgeben von Fehlercodes (4xx/5xx) veranlasst Resend, den Webhook zu wiederholen, was bei dauerhaft ungültigen Payloads zu Wiederholungsstürmen führen kann. Fehler werden zum Debugging protokolliert, aber die Antwort bestätigt stets den Empfang. Das gleiche Muster wird für Clerk- und Lemon-Squeezy-Webhooks verwendet.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:
PENDING → SENT → DELIVERED → OPENED → CLICKED. 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-Typ | Zeitfenster | Auswirkung |
|---|---|---|
| Willkommen | 24 Stunden | Verhindert doppelte Willkommens-E-Mails bei Neuregistrierung |
| Kontaktbestätigung | 5 Minuten | Verhindert Kontaktformular-Spam |
| Abonnement-Events | 30 Minuten | Verhindert doppelte Abonnement-Benachrichtigungen |
| Vorlagen-E-Mails | 30 Minuten | Verhindert doppelte Credit-/Trial-E-Mails |
| Test-E-Mail | Keine | Wird immer gesendet (nur in der Entwicklung) |
E-Mail-Rate-Limiting funktioniert auf zwei Ebenen: (1) Vorlagen-spezifische Limits in
service.ts verhindern doppelte Sendungen, und (2) API-Route-Rate-Limiting über withRateLimit('email', handler) verhindert Missbrauch der E-Mail-API-Endpunkte. Informationen zur API-Rate-Limiting-Konfiguration findest du unter Caching & Redis.Umgebungsvariablen
| Variable | Erforderlich | Standard | Zweck |
|---|---|---|---|
RESEND_API_KEY | Ja | test_key | Resend-API-Schlüssel zum Senden von E-Mails |
RESEND_FROM_EMAIL | Ja | noreply@example.com | Standard-Absender-E-Mail-Adresse |
RESEND_WEBHOOK_SECRET | Nein | — | HMAC-Secret zur Webhook-Signaturverifizierung |
NEXT_PUBLIC_CONTACT_EMAIL | Nein | Fallback auf RESEND_FROM_EMAIL | Admin-E-Mail für Kontaktformular-Benachrichtigungen |
NEXT_PUBLIC_APP_URL | Ja | — | Basis-URL für Links in E-Mail-Vorlagen |
Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/src/lib/email/service.ts | Übergeordnete E-Mail-Funktionen (sendWelcomeEmail, sendContactConfirmation usw.) |
apps/boilerplate/src/lib/email/client.ts | Resend-SDK-Client mit Retry-Logik und exponentiellem Backoff |
apps/boilerplate/src/lib/email/types.ts | TypeScript-Typen für E-Mail-Optionen, Webhook-Daten |
apps/boilerplate/src/lib/db/queries/email-logs.ts | Datenbankabfragen 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.ts | Webhook-Endpunkt mit Signaturverifizierung |