Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
Lernen Sie, wie Sie sysctls, ulimits, cgroups und Kubernetes-Ressourcensteuerungen optimieren, um maximale Performance, Stabilität und Sicherheit für Production-Workloads zu gewährleisten.
Als ich anfing, digitale Produkte auf graphwiz.ai und tobias-weiss.org zu verkaufen, hatte ich eine klare Anforderung: Kunden sollten auf einer von Stripe gehosteten Checkout-Seite landen, bezahlen und sofort ihren Download erhalten – und das alles, ohne dass ich ein Warenkorb-UI bauen, mich um die PCI-Compliance kümmern oder Kreditkartennummern speichern muss.
Stripe Checkout war die offensichtliche Wahl. Es übernimmt das Payment-UI, unterstützt mehrere Währungen (EUR/USD), „Pay What You Want“-Preise, und die Infrastruktur von Stripe regelt die Compliance. Aber die saubere Integration in eine Next.js-App, die Anbindung von Webhooks für die Auslieferung und die Sicherstellung, dass die CSP Stripe.js nicht blockiert, erforderten eine sorgfältige Architektur.
Dieser Artikel beschreibt genau, wie ich es umgesetzt habe – die Architektur-Entscheidungen, die Code-Patterns und die Lektionen, die ich dabei gelernt habe.
User → BuyButton → /api/stripe/create-checkout → Stripe Checkout (hosted)
↓
Success URL ← stripe.com
↓
Success Page (verify session)
↓
Download + Upsell
↓
Webhook (fulfillment + email)
Der Flow ist bewusst zustandslos (stateless) gestaltet. Es werden keine Kundendaten gespeichert, bis die Zahlung erfolgreich war. Die Checkout-Session trägt alles – Produktname, Slug, Download-URL – in ihren Metadaten.
Ich habe mehrere Plattformen evaluiert, bevor ich dies gebaut habe:
| Ansatz | Vorteile | Nachteile |
|---|---|---|
| Stripe Checkout (meine Wahl) | Volle Kontrolle, keine monatliche Gebühr, eigenes Branding | Store-UI muss gebaut werden, Webhooks-Handling |
| Shopify | Schlüsselfertiger Store, Analytics | Monatliche Gebühren ($39+), Ecosystem-Lock-in |
| Gumroad | Sofortiger Setup, integrierte E-Mails | 8,5 % + $0,30 pro Verkauf, begrenzte Anpassung |
| LemonSqueezy | Steuerhandling, E-Mail-Versand | Drittanbieter-Abhängigkeit, weniger Kontrolle |
Für eine DevOps-orientierte Seite war es die richtige Entscheidung, die Integration selbst zu besitzen. Die Stripe-API ist exzellent, und die monatlichen Ersparnisse bei steigender Skalierung rechtfertigen die Entwicklungszeit.
Das Herzstück der Integration ist eine einzige API-Route. Sie empfängt eine Price-ID und Produkt-Metadaten, erstellt eine Stripe Checkout Session und gibt die Redirect-URL zurück.
// app/api/stripe/create-checkout/route.ts
import { NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, productName, productSlug, customerEmail } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
customer_email: customerEmail,
success_url: `${origin}/store/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/store`,
metadata: { productName, productSlug, downloadUrl },
});
return NextResponse.json({ checkoutURL: session.url });
}
Wichtige Details:
{CHECKOUT_SESSION_ID} ist ein Stripe-Template – Stripe ersetzt dies zum Zeitpunkt des Redirects durch die tatsächliche Session-ID.Nach der Zahlung leitet Stripe an /store/success?session_id=cs_... weiter. Die Success-Page verifiziert die Session serverseitig:
// app/store/success/page.tsx
const session = await retrieveCheckoutSession(sessionId);
productName = session.metadata?.productName;
if (session.payment_status === "paid") {
// Generate download token
downloadToken = Buffer.from(
JSON.stringify({
session_id: sessionId,
product_slug: productSlug,
timestamp: Date.now(),
}),
).toString("base64");
}
Der Download-Token ist selbstbeschreibend (self-contained) – er benötigt keine Datenbank. Die Download-API-Route dekodiert und verifiziert den Token bei jeder Anfrage gegen die Stripe-API. Das bedeutet, dass Token auch Server-Neustarts überstehen (by design zustandslos).
Der Webhook empfängt Events von Stripe nach Abschluss des Checkouts. Dies ist der „zuverlässige Pfad“ – Stripe versucht die Zustellung erneut, falls der Endpunkt down ist, im Gegensatz zum Redirect der Success-Page, dem der Kunde sofort folgen muss.
// app/api/stripe/webhook/route.ts
const event = constructWebhookEvent(body, signature);
switch (event.type) {
case "checkout.session.completed": {
// Log purchase to purchases.json
// Send confirmation email via local postfix relay
// Generate download token
break;
}
}
Der Webhook muss die Stripe-Signatur mit constructWebhookEvent() verifizieren. Ohne dies könnte jeder, der Ihre Webhook-URL findet, Events fälschen und kostenlose Downloads auslösen.
Ein häufiger Fehler: Das Webhook-Secret befindet sich in Ihrer .env.production, wird aber nicht automatisch im Stripe-Dashboard konfiguriert. Sie müssen im Stripe-Dashboard (oder via API) einen Webhook-Endpunkt erstellen, der auf https://yoursite.com/api/stripe/webhook verweist.
curl https://api.stripe.com/v1/webhook_endpoints \
-u "sk_live_..." \
-d "url=https://yoursite.com/api/stripe/webhook" \
-d "enabled_events[]=checkout.session.completed" \
-d "enabled_events[]=checkout.session.expired"
Das von diesem API-Aufruf zurückgegebene whsec_... Secret muss mit Ihrem STRIPE_WEBHOOK_SECRET übereinstimmen. Sie bilden ein passendes Paar – wenn Sie eines regenerieren, wird das andere ungültig.
Wenn der Webhook ausgelöst wird, versende ich eine Bestätigungs-E-Mail über ein lokales Postfix-Relay, das auf dem Server läuft. Dadurch werden Drittanbieter-E-Mail-Dienste vermieden:
const transporter = nodemailer.createTransport({
host: "127.0.0.1",
port: 2525,
secure: false,
ignoreTLS: true,
});
await transporter.sendMail({
from: '"Store" <noreply@tobias-weiss.org>',
replyTo: customerEmail,
to: process.env.CONTACT_EMAIL,
subject: "New Purchase: " + productName,
text: "..." + downloadLink + "...",
});
Die E-Mail enthält den Download-Link als Backup – selbst wenn der Kunde den Browser nach der Zahlung schließt, erhält er den Link in seinem Posteingang.
Stripe.js erfordert spezifische CSP-Direktiven. Hier ist die restriktivste Konfiguration, die funktioniert:
script-src 'self' 'unsafe-inline' https://js.stripe.com;
connect-src 'self' https://api.stripe.com;
frame-src https://js.stripe.com;
img-src https://*.stripe.com;
Hinweis: 'unsafe-inline' ist für Next.js Runtime-Chunks und Stripe.js erforderlich. Sie können dies nicht entfernen, ohne eines der Frameworks zu beeinträchtigen.
Stripe begrenzt Metadatenwerte auf jeweils 500 Zeichen. Wenn Ihr Produktname lang ist, kürzen Sie ihn. Das downloadUrl muss kurz sein.
Alle Preis-IDs im Test-Modus (price_1Test...) sind vollständig getrennt vom Live-Modus (price_1Live...). Sie müssen in beiden Umgebungen separate Preise erstellen. Das Gleiche gilt für Webhook-Endpunkte – Stripe hat separate Webhook-Konfigurationen für Test und Live.
{CHECKOUT_SESSION_ID} TemplateDiese Template-Variable funktioniert nur in der success_url und cancel_url einer Checkout-Session. Es handelt sich nicht um eine allgemeine Stripe-Funktion. Ich hatte sie anfangs weggelassen – die Erfolgsseite konnte den Kauf nicht verifizieren, da sie keine Session-ID hatte.
Falls Sie jemals von Checkout zu Stripe Elements (eingebettetes Zahlungsformular) wechseln, benötigen Sie zusätzliche CSP-Direktiven für https://m.stripe.com und https://js.stripe.com. Checkout verarbeitet das Zahlungsformular auf der Domain von Stripe, weshalb dies einfacher ist.
Stripe versucht die Zustellung fehlgeschlagener Webhooks bis zu 3 Tage lang zu wiederholen. Ihr Handler muss idempotent sein – die zweifache Verarbeitung desselben Events sollte keine zusätzlichen Seiteneffekte haben. Meine Implementierung prüft auf bestehende Einträge in purchases.json, bevor Duplikate hinzugefügt werden.
Das beschriebene Stripe-Integrationsmuster ist wiederverwendbar. Hier sind Produkte, die ich auf dieser Basis entwickeln möchte:
Jedes dieser Produkte würde natürlich neben den bestehenden DevOps-Stacks in den Store passen.
Stripe Checkout + Next.js ist eine leistungsstarke Kombination für den Verkauf digitaler Produkte. Die Architektur ist simpel – API-Route → Stripe Checkout → Erfolgsseite + Webhook – und skaliert von 1 € Community-Supporter-Tokens bis hin zu 79 € Produktions-Guides.
Der Schlüssel zum Erfolg in der Produktion:
.env.production aufbewahrenDer vollständige Code für beide Stores ist Open-Source unter github.com/tobias-weiss-ai-xr verfügbar.