{
  "id": "add-stripe-checkout-to-nextjs",
  "type": "prompts",
  "category": "prompts",
  "locale": "de",
  "url": "/de/prompts/add-stripe-checkout-to-nextjs",
  "title": "Prompt zum Hinzufügen von Stripe Checkout zu einer Next.js App",
  "description": "KI-Agent-Prompt zum Hinzufügen von Stripe Checkout mit Webhook-Handling, Kundenportal und Abonnementstatus zu einem Next.js App Router-Projekt.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "PostgreSQL",
    "TypeScript"
  ],
  "tags": [
    "nextjs",
    "typescript",
    "postgres",
    "auth",
    "security"
  ],
  "difficulty": "hard",
  "updated": "2026-06-08",
  "markdown": "Geben Sie diesen Prompt Ihrem Agenten, um den vollständigen Stripe-Abrechnungsablauf zu implementieren — Checkout-Sitzungserstellung, Webhook-Verarbeitung, Kundenportal und Abonnementstatus-Beschränkung — mit ordnungsgemäßer Webhook-Signaturverifikation und ohne Geheimnisse im Client-Code.\n\n## Haupt-Prompt\n\n```txt title=\"Main Prompt\"\nYou are working in a Next.js 15 App Router project with TypeScript and PostgreSQL.\nAuth is already set up with a `getSession()` helper. The pricing page already defines\n`PLANS` with Stripe Price IDs.\n\nTask: wire up Stripe Checkout for subscription billing.\n\nRequirements:\n- Install `stripe` (server-only) and `@stripe/stripe-js` (client). Do NOT import `stripe` in\n  Client Components or expose `STRIPE_SECRET_KEY` to the browser.\n- Create `src/lib/stripe.ts`: export a singleton Stripe client using `STRIPE_SECRET_KEY`.\n- Create a Server Action `src/lib/actions/create-checkout.ts`:\n  - Get the current user session; return an error if not authenticated.\n  - Create a Stripe Checkout Session in `subscription` mode.\n  - Set `success_url` to `/dashboard?session_id={CHECKOUT_SESSION_ID}`.\n  - Set `cancel_url` to `/pricing`.\n  - Store the Stripe `customerId` in the `users` table (`stripe_customer_id` column).\n  - Return the Checkout Session URL.\n- Create `src/app/api/stripe/webhook/route.ts`:\n  - Read the raw body using `request.text()`.\n  - Verify the signature using `stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET)`.\n  - Handle events: `checkout.session.completed`, `customer.subscription.updated`,\n    `customer.subscription.deleted`.\n  - On each event, update the `users` table: set `subscription_status` and `subscription_tier`.\n  - Return `{ received: true }` with status 200.\n  - On signature failure, return 400.\n- Create `src/lib/actions/create-portal.ts`: create a Stripe Customer Portal session and return the URL.\n- Add a PostgreSQL migration `migrations/0011_add_stripe_columns.sql` adding\n  `stripe_customer_id TEXT`, `subscription_status TEXT`, `subscription_tier TEXT` to `users`.\n- Add all Stripe keys to `.env.example`: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`,\n  `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`.\n- Do NOT use the deprecated `stripe.charges` API. Use Payment Intents / Checkout Sessions only.\n\nStop and list all planned file changes before writing any code.\n```\n\n## Implementierungshinweise\n\n- Die Webhook-Signaturverifikation erfordert den **rohen** Anfragekörper — wenn Sie ihn zuerst als JSON parsen, schlägt die Signaturprüfung fehl. Verwenden Sie `request.text()` nicht `request.json()`.\n- Der Webhook-Handler muss über eine Routenkonfiguration von den Next.js-Größenbeschränkungen für den Körper ausgenommen werden:\n  `export const config = { api: { bodyParser: false } }` (Pages Router) — im App Router verwenden Sie\n  `export const dynamic = 'force-dynamic'` und lesen Sie den Stream direkt.\n- Testen Sie Webhooks lokal mit der Stripe-CLI: `stripe listen --forward-to localhost:3000/api/stripe/webhook`.\n- Speichern Sie `stripe_customer_id` beim ersten Checkout, um die Erstellung doppelter Stripe-Kunden zu vermeiden.\n\n## Erwartete Dateiänderungen\n\n```txt\nsrc/lib/stripe.ts                           (new)\nsrc/lib/actions/create-checkout.ts          (new — Server Action)\nsrc/lib/actions/create-portal.ts            (new — Server Action)\nsrc/app/api/stripe/webhook/route.ts         (new — Route Handler)\nmigrations/0011_add_stripe_columns.sql      (new)\n.env.example                                (edited)\npackage.json                                (edited)\n```\n\n## Akzeptanzkriterien\n\n- Ein Klick auf einen Preis-CTA leitet zu Stripe Checkout für den richtigen Plan weiter.\n- Das Abschließen eines Test-Checkouts aktualisiert `subscription_status = 'active'` in PostgreSQL.\n- Der Stripe-Kundenportal-Link leitet zum von Stripe gehosteten Portal weiter.\n- Das Senden eines Test-`customer.subscription.deleted`-Ereignisses über die Stripe-CLI setzt `subscription_status = 'canceled'`.\n- Ein Webhook mit einer ungültigen Signatur gibt HTTP 400 zurück.\n\n## Testbefehle\n\n```bash\nbun add stripe @stripe/stripe-js\npsql \"$DATABASE_URL\" -f migrations/0011_add_stripe_columns.sql\nbun run typecheck\nbun run dev &\nstripe listen --forward-to localhost:3000/api/stripe/webhook\nstripe trigger checkout.session.completed\n# verify users table updated\npsql \"$DATABASE_URL\" -c \"SELECT stripe_customer_id, subscription_status FROM users LIMIT 5;\"\n```\n\n## Häufige KI-Fehler\n\n- Importieren von `stripe` (Server-SDK) in eine Client-Komponente, wodurch `STRIPE_SECRET_KEY` offengelegt wird.\n- Parsen des Webhook-Körpers als JSON vor der Signaturverifikation, wodurch alle Webhook-Validierungen fehlschlagen.\n- Erstellen eines neuen Stripe-Kunden bei jedem Checkout anstatt `stripe_customer_id` wiederzuverwenden.\n- Verwenden von `stripe.charges.create` (veraltet) anstelle von `stripe.checkout.sessions.create`.\n\n## Fix-Prompt\n\n```txt title=\"Fix Prompt\"\nWebhook signature verification fails with `No signatures found matching the expected signature`.\nFix in order:\n1. In the webhook route, replace `await request.json()` with `const body = await request.text()`.\n2. Ensure `stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)` receives\n   the raw string body, not a parsed object.\n3. Add `export const dynamic = 'force-dynamic'` at the top of the route file.\nShow only the corrected route handler diff.\n```"
}