Stripe Suscripciones — Configuración
Módulo autocontenido para pagos recurrentes: crear, leer, cancelar, pausar, reanudar, cambiar de plan, cambiar la cantidad, trials, estado, renovación y expiración.
Está separado de lib/stripe/ (pagos únicos) y lib/stripe-billing/
(portal del cliente, facturas, métodos de pago): no comparten código ni
endpoint de webhook. Los tres envuelven la misma cuenta de Stripe pero mantienen
sus responsabilidades independientes.
Supuesto: este módulo opera sobre un priceId existente (creado en
el Stripe Dashboard, o por lo que sea que gestione tu catálogo de productos).
No crea Products ni Prices.
Dependencias
stripe— SDK del lado del servidor, usado enlib/stripe-subscriptions/service.ts@stripe/stripe-js— SDK del lado del cliente, usado enlib/stripe-subscriptions/client.ts
No se instalan automáticamente. Si ya los instalaste para los otros módulos, estás cubierto: son los mismos paquetes.
Mapa de archivos
lib/stripe-subscriptions/
├── service.ts # Internal logic + Stripe SDK instance. Don't touch/import directly elsewhere.
├── actions.ts # Server Actions — import into Server Components.
├── types.ts # Shared types — safe to import anywhere.
└── client.ts # Client-side helpers — import into Client Components.
app/api/webhooks/stripe-subscriptions/
└── route.ts # Subscriptions webhook endpoint. Register this URL in your Stripe Dashboard.
.env.local # Adds STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRET (reuses
# STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
# if already set up for the other modules).
1. Variables de entorno
STRIPE_SECRET_KEY y NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY se comparten
entre los tres módulos de Stripe: definilas una sola vez. Además, agregá:
| Variable | Dónde conseguirla |
|---|---|
STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRET | Dashboard → Developers → Webhooks (creá un tercer endpoint, separado) |
2. Webhooks
Agregá un endpoint separado en el Stripe Dashboard que apunte a:
https://yourdomain.com/api/webhooks/stripe-subscriptions
Suscribilo como mínimo a:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted— tu señal real de "expiración"customer.subscription.trial_will_endcustomer.subscription.pausedcustomer.subscription.resumedinvoice.paid— tu señal real de "renovación" para los cobros recurrentesinvoice.payment_failed
Para probar en local:
stripe listen --forward-to localhost:3000/api/webhooks/stripe-subscriptions
La lógica de negocio de lo que pasa con cada evento va dentro de
stripeSubscriptionsService.handleWebhookEvent en service.ts: la única
parte de ese archivo pensada para extenderse a medida que tu app crece.
3. Uso
Crear una suscripción (con o sin trial)
import { createSubscription } from "@/lib/stripe-subscriptions/actions";
const result = await createSubscription({
customerId,
priceId,
trialPeriodDays: 14, // omit for no trial
});
if (result.success && result.data.clientSecret) {
// No trial (or a card needing 3D Secure): confirm the first charge client-side.
}
Del lado del cliente, solo cuando clientSecret no es null:
"use client";
import { confirmSubscriptionPayment } from "@/lib/stripe-subscriptions/client";
await confirmSubscriptionPayment(clientSecret, paymentMethodId);
Una suscripción creada con un trial y sin clientSecret ya está en
trialing: no hay nada que confirmar hasta que termine el trial y Stripe la
cobre automáticamente (se maneja mediante los eventos de webhook
invoice.paid / invoice.payment_failed).
Leer la suscripción / el estado
import { getSubscription, getSubscriptionStatus } from "@/lib/stripe-subscriptions/actions";
const sub = await getSubscription(subscriptionId);
const status = await getSubscriptionStatus(subscriptionId);
Cancelar
import { cancelSubscription } from "@/lib/stripe-subscriptions/actions";
await cancelSubscription({ subscriptionId }); // immediate
await cancelSubscription({ subscriptionId, atPeriodEnd: true }); // at period end
Pausar / reanudar
import { pauseSubscription, resumeSubscription } from "@/lib/stripe-subscriptions/actions";
await pauseSubscription(subscriptionId);
await resumeSubscription(subscriptionId);
Upgrade / downgrade / cambio de plan
Los tres son la misma operación: cambiar el price del ítem de la suscripción. Decidí la etiqueta ("upgrade" vs "downgrade") en tu UI según la diferencia de precio; a la función en sí no le importa:
import { changeSubscriptionPlan } from "@/lib/stripe-subscriptions/actions";
await changeSubscriptionPlan({
subscriptionId,
newPriceId,
prorationBehavior: "create_prorations", // default; see Stripe docs for alternatives
});
Cambiar la cantidad (por ejemplo, seats)
import { changeSubscriptionQuantity } from "@/lib/stripe-subscriptions/actions";
await changeSubscriptionQuantity({ subscriptionId, quantity: 5 });
Información de renovación / expiración
No existe una acción de "renovar" en la API de Stripe: la renovación ocurre automáticamente con el ciclo de facturación. Estas dos leen el estado actual en su lugar:
import { getRenewalInfo, getExpirationInfo } from "@/lib/stripe-subscriptions/actions";
const renewal = await getRenewalInfo(subscriptionId);
// { willRenew, currentPeriodEnd, nextPaymentAttempt }
const expiration = await getExpirationInfo(subscriptionId);
// { cancelAtPeriodEnd, expiresAt, status }
Para los eventos de renovación/expiración en el momento en que ocurren, escuchá
invoice.paid (renovada) y customer.subscription.deleted (expirada) en
el handler del webhook.
Manejo de errores
Cada función de actions.ts y client.ts devuelve:
{ success: true, data: T } | { success: false, error: string }
Verificá siempre result.success antes de leer result.data: nada
lanza errores a través del límite del módulo.
Notas
service.tses el único archivo que importa el paquetestripeo leeSTRIPE_SECRET_KEY/STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRET.- Este módulo no crea Products/Prices ni Customers: se asume que ambos ya existen.
- La pausa usa
pause_collection: { behavior: "void" }(no se generan facturas mientras está pausada, no se debe nada por ese tiempo). Revisá la documentación de Stripe si preferís el comportamientomark_uncollectibleokeep_as_draft.