Saltar al contenido
Todos los artículos

Webhooks idempotentes de Stripe y Lemon Squeezy en Next.js

Cómo verificar firmas en bruto de webhooks, deduplicar entregas repetidas en la frontera de la base de datos y evitar que suscripciones solapadas revoquen el acceso de pago.

el equipo de supastack2 min de lectura

How do I handle Stripe and Lemon Squeezy webhooks idempotently in Next.js?

It depends on your billing providers, but the core architecture is identical: verify the raw payload signature before parsing JSON, normalize provider events into a shared internal contract, and enforce idempotency at the database boundary using the provider's event or order ID. Never grant product entitlements from a client-side checkout redirect; let the verified webhook transactionally update subscription state and entitlements once. supastack prewires this pipeline for Stripe, Lemon Squeezy, and mobile RevenueCat, though single-plan apps can start with a simpler handler.

Verifica el payload en bruto antes de tocar el JSON

Cada proveedor de pagos firma la secuencia exacta de bytes que envía por la red. Si un controlador de ruta analiza await request.json() y vuelve a serializar el objeto antes de comprobar la firma, las diferencias de orden de claves o espacios invalidarán webhooks legítimos o tentarán al equipo a desactivar la verificación en staging.

Lee el cuerpo en bruto una sola vez con await request.text() en tu controlador POST del App Router de Next.js, verifica la firma criptográfica con el secreto de servidor y rechaza cualquier fallo con HTTP 400 antes de ejecutar una sola consulta. La guía del módulo de facturación muestra cómo supastack aísla la verificación de firmas detrás de un único punto de entrada BFF.

export async function POST(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get("stripe-signature");
  if (!signature) return new Response("Missing signature", { status: 400 });

  const event = stripe.webhooks.constructEvent(
    rawBody,
    signature,
    env.STRIPE_WEBHOOK_SECRET,
  );
  await handleNormalizedBillingEvent(event);
  return Response.json({ received: true });
}
  • Lee el cuerpo de la petición con request.text(), nunca vuelvas a serializar un JSON ya analizado.
  • Mantén STRIPE_WEBHOOK_SECRET y los secretos de firma de Lemon Squeezy exclusivamente en la configuración del servidor.
  • Usa comparación en tiempo constante (crypto.timingSafeEqual) al verificar firmas HMAC manualmente.

Deduplica en la frontera de la base de datos, no en memoria

Stripe y Lemon Squeezy reintentan los webhooks cada vez que un despliegue se reinicia, una función serverless agota el tiempo o un salto de red pierde la respuesta. Los conjuntos en memoria o las claves de caché efímeras desaparecen entre instancias y no impiden que dos reintentos simultáneos otorguen licencias duplicadas o envíen dos correos de recibo.

Registra el ID de evento del proveedor o el estado de liquidación del pedido dentro de la misma transacción de Postgres que modifica el acceso de la suscripción. Un INSERT ... ON CONFLICT DO NOTHING atómico o una actualización condicional sobre status != 'paid' convierte una repetición concurrente en una operación sin efecto. Al diseñar los planes que esos webhooks activan, el generador de páginas de precios SaaS ayuda a modelar niveles y reglas por asiento desde el principio.

const settled = await db.transaction(async (tx) => {
  const [inserted] = await tx
    .insert(billingWebhookEvents)
    .values({ providerEventId: event.id, provider: "stripe" })
    .onConflictDoNothing({ target: billingWebhookEvents.providerEventId })
    .returning({ id: billingWebhookEvents.id });

  if (!inserted) return false; // Already processed
  await applyWorkspaceEntitlement(tx, event);
  return true;
});

Gestiona eventos fuera de orden y suscripciones solapadas

No hay garantía de que los webhooks lleguen en el orden en que el usuario hizo clic. customer.subscription.updated puede aterrizar antes que checkout.session.completed, y un cliente que abre dos pestañas de pago por error puede tener brevemente dos suscripciones en el mismo workspace.

Vincula las cancelaciones y los fallos de pago al ID de suscripción específico que originó el evento. Antes de revocar el derecho de pago de un workspace, comprueba si otra suscripción activa sigue cubriendo ese workspace; de lo contrario, cancelar una suscripción duplicada elimina en silencio el acceso por el que el cliente sigue pagando.

  • Indexa el estado de la suscripción por providerSubscriptionId, no solo por workspaceId.
  • Mantén activo el derecho de pago del workspace mientras al menos una suscripción esté active o trialing.
  • Verifica que el importe pagado y la divisa coincidan con el SKU esperado antes de completar pedidos de pago único.

Preguntas frecuentes sobre webhooks de facturación SaaS

Estas preguntas aparecen cada vez que un equipo pasa la facturación de las pruebas en sandbox al tráfico real de producción.

  • ¿Puedo desbloquear funciones de pago en la redirección de éxito del checkout? — No. Las URLs de redirección pueden guardarse en favoritos, compartirse o visitarse antes de que se liquiden métodos de pago asíncronos. Usa la página de éxito para consultar el estado del servidor mientras el webhook verificado otorga el acceso.
  • ¿Qué código de estado debe devolver un webhook duplicado? — Devuelve 200 OK en cuanto tu base de datos confirme que el evento ya fue procesado. Devolver 4xx o 5xx le indica al proveedor que siga reintentando.
  • ¿Cómo pruebo los reintentos de webhooks en local? — Escribe pruebas unitarias deterministas que envíen exactamente el mismo payload firmado dos veces y comprueba que solo se genere una licencia, un derecho o un correo transaccional.
  • ¿Cómo encajan las compras integradas móviles en el mismo modelo? — Dirige las notificaciones de servidor de RevenueCat hacia la misma tabla idempotente de derechos para que las compras web y móviles nunca se sobrescriban entre sí; consulta la lista de verificación de lanzamiento para las comprobaciones previas al estreno.

Preguntas frecuentes

14 preguntas

¿Qué es supastack?

supastack es una base modular para construir productos SaaS con IA. Reúne la infraestructura común para que puedas centrarte en las funciones propias de tu producto.

¿Soporta web y móvil?

Sí. supastack incluye apps Next.js y Expo como puntos de partida para experiencias web y móviles.

¿Qué autenticación incluye?

Better Auth cubre correo/contraseña, OTP e inicio social. Los espacios de trabajo personales se aprovisionan con la cuenta para que los registros permanezcan delimitados desde el primer día.

¿Cómo se soporta la IA?

supastack incluye IA multi-proveedor configurable, seguimiento de uso, presupuestos y soporte cifrado de claves propias (BYOK).

¿Cómo se gestionan las suscripciones?

Para productos construidos sobre el kit, la facturación Stripe, las suscripciones y los permisos forman la base del acceso de pago. Los secretos de proveedor permanecen en el servidor.

¿Dónde vive el código propio de mi producto?

Las funciones propias del producto permanecen en un codebase separado, mientras el producto configura e integra los módulos base de supastack.

¿Cómo funciona el precio de supastack?

El kit es una compra única con actualizaciones de por vida: Stack único a 249 $ —a elegir entre los repositorios Next.js (web), Expo (iOS y Android) o Tauri (macOS y Windows)—, Doble stack a 398 $ con dos repositorios a elegir, o Paquete completo a 399 $ con los tres repositorios y el monorepo completo, más 75 $ por plaza de desarrollador adicional. Sin suscripción al kit. El pago en este sitio entrega tu clave de licencia por correo, con acceso al repositorio por invitación de GitHub.

¿Cómo delimitan los espacios de trabajo mis datos?

Los registros del usuario se delimitan por workspaceId. Identidad, permisos, uso de IA, eventos de facturación y entradas de auditoría permanecen dentro del límite del espacio personal.

¿Cómo funcionan los jobs en segundo plano?

Trigger.dev es el runtime de jobs explícito. El contrato JobRunner solo promete enqueue, status y cancel — la semántica de workflow sigue siendo específica y explícita de Trigger.dev.

¿En qué se diferencia el email de la divulgación?

El email transaccional cubre flujos de producto como avisos de finalización de tareas. La divulgación en frío y las campañas pertenecen al producto, fuera del módulo de email de este kit.

¿Cómo funcionan las compras móviles?

La app móvil incluye compras nativas y restauración con RevenueCat, con acceso verificado por tu servidor. Configura las cuentas de las tiendas y completa las pruebas en dispositivos antes de publicar. La facturación web es independiente.

¿Dónde residen las claves de los proveedores de IA?

Las credenciales BYOK están cifradas y permanecen en el servidor. Las claves se saneen en los logs; el uso y los presupuestos siguen visibles a nivel de espacio de trabajo.

¿Qué métodos de pago acepta el checkout?

El checkout alojado en Stripe acepta tarjetas y, una vez activadas las billeteras en la cuenta de Stripe, también puede ofrecer Alipay y WeChat Pay — muy útil para compradores en China. Las autorizaciones de billetera se confirman de forma asíncrona, por lo que la página de la licencia se actualiza en cuanto se liquida el pago.

¿Puedo obtener un reembolso?

Una vez que tienes acceso al repositorio, supastack es tuyo para siempre, por lo que no se puede reembolsar. Pero no te preocupes: obtienes el código fuente completo con actualizaciones de por vida para crear y lanzar productos ilimitados para ti o tus clientes. Solo no puedes revender el código ni redistribuir el repositorio como plantilla o starter kit. Una excepción es el cashback de lanzamiento — lanza un producto real creado con supastack, compártelo y alcanza 5.000 visualizaciones orgánicas o 50 Me gusta orgánicos en un plazo de 60 días para recuperar el 50 % de lo que pagaste; consulta los términos.

ES