# Prompt para añadir Stripe Checkout a una aplicación Next.js

> Prompt para agente de IA para añadir Stripe Checkout con manejo de webhooks, portal de cliente y estado de suscripción a un proyecto Next.js App Router.

**Type:** Prompt  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** Next.js, PostgreSQL, TypeScript  
**Difficulty:** hard  
**Updated:** 2026-06-08

---

Entrega este prompt a tu agente para implementar el flujo completo de facturación de Stripe — creación de sesión de checkout, procesamiento de webhooks, portal de cliente y control de estado de suscripción — con verificación adecuada de firma de webhooks y sin secretos en el código del cliente.

## Prompt Principal

```txt title="Main Prompt"
You are working in a Next.js 15 App Router project with TypeScript and PostgreSQL.
Auth is already set up with a `getSession()` helper. The pricing page already defines
`PLANS` with Stripe Price IDs.

Task: wire up Stripe Checkout for subscription billing.

Requirements:
- Install `stripe` (server-only) and `@stripe/stripe-js` (client). Do NOT import `stripe` in
  Client Components or expose `STRIPE_SECRET_KEY` to the browser.
- Create `src/lib/stripe.ts`: export a singleton Stripe client using `STRIPE_SECRET_KEY`.
- Create a Server Action `src/lib/actions/create-checkout.ts`:
  - Get the current user session; return an error if not authenticated.
  - Create a Stripe Checkout Session in `subscription` mode.
  - Set `success_url` to `/dashboard?session_id={CHECKOUT_SESSION_ID}`.
  - Set `cancel_url` to `/pricing`.
  - Store the Stripe `customerId` in the `users` table (`stripe_customer_id` column).
  - Return the Checkout Session URL.
- Create `src/app/api/stripe/webhook/route.ts`:
  - Read the raw body using `request.text()`.
  - Verify the signature using `stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET)`.
  - Handle events: `checkout.session.completed`, `customer.subscription.updated`,
    `customer.subscription.deleted`.
  - On each event, update the `users` table: set `subscription_status` and `subscription_tier`.
  - Return `{ received: true }` with status 200.
  - On signature failure, return 400.
- Create `src/lib/actions/create-portal.ts`: create a Stripe Customer Portal session and return the URL.
- Add a PostgreSQL migration `migrations/0011_add_stripe_columns.sql` adding
  `stripe_customer_id TEXT`, `subscription_status TEXT`, `subscription_tier TEXT` to `users`.
- Add all Stripe keys to `.env.example`: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`,
  `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`.
- Do NOT use the deprecated `stripe.charges` API. Use Payment Intents / Checkout Sessions only.

Stop and list all planned file changes before writing any code.
```

## Notas de Implementación

- La verificación de firma de webhook requiere el cuerpo **crudo** de la solicitud — si lo analizas como JSON primero, la verificación de firma fallará. Usa `request.text()` no `request.json()`.
- El manejador de webhook debe excluirse de los límites de tamaño del cuerpo de Next.js mediante una configuración de ruta: `export const config = { api: { bodyParser: false } }` (Pages Router) — en App Router, usa `export const dynamic = 'force-dynamic'` y lee el flujo directamente.
- Prueba webhooks localmente con la CLI de Stripe: `stripe listen --forward-to localhost:3000/api/stripe/webhook`.
- Almacena `stripe_customer_id` en el primer checkout para evitar crear clientes duplicados en Stripe.

## Cambios de Archivos Esperados

```txt
src/lib/stripe.ts                           (new)
src/lib/actions/create-checkout.ts          (new — Server Action)
src/lib/actions/create-portal.ts            (new — Server Action)
src/app/api/stripe/webhook/route.ts         (new — Route Handler)
migrations/0011_add_stripe_columns.sql      (new)
.env.example                                (edited)
package.json                                (edited)
```

## Criterios de Aceptación

- Hacer clic en un CTA de precios redirige a Stripe Checkout para el plan correcto.
- Completar un checkout de prueba actualiza `subscription_status = 'active'` en PostgreSQL.
- El enlace al Portal de Cliente de Stripe redirige al portal alojado por Stripe.
- Enviar un evento de prueba `customer.subscription.deleted` mediante la CLI de Stripe establece `subscription_status = 'canceled'`.
- Un webhook con firma inválida devuelve HTTP 400.

## Comandos de Prueba

```bash
bun add stripe @stripe/stripe-js
psql "$DATABASE_URL" -f migrations/0011_add_stripe_columns.sql
bun run typecheck
bun run dev &
stripe listen --forward-to localhost:3000/api/stripe/webhook
stripe trigger checkout.session.completed
# verify users table updated
psql "$DATABASE_URL" -c "SELECT stripe_customer_id, subscription_status FROM users LIMIT 5;"
```

## Errores Comunes de IA

- Importar `stripe` (SDK del servidor) en un Componente Cliente, exponiendo `STRIPE_SECRET_KEY`.
- Analizar el cuerpo del webhook como JSON antes de la verificación de firma, causando que todas las validaciones de webhook fallen.
- Crear un nuevo cliente de Stripe en cada checkout en lugar de reutilizar `stripe_customer_id`.
- Usar `stripe.charges.create` (obsoleto) en lugar de `stripe.checkout.sessions.create`.

## Prompt de Corrección

```txt title="Fix Prompt"
Webhook signature verification fails with `No signatures found matching the expected signature`.
Fix in order:
1. In the webhook route, replace `await request.json()` with `const body = await request.text()`.
2. Ensure `stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)` receives
   the raw string body, not a parsed object.
3. Add `export const dynamic = 'force-dynamic'` at the top of the route file.
Show only the corrected route handler diff.
```