BusiRocket

Artículo

Los pagos de Stripe en Lovable no funcionan en producción: qué revisar, en orden

Publicado el

Stripe en una app de Lovable suele funcionar a la primera, en modo test, con la tarjeta 4242 4242 4242 4242. El problema empieza cuando pasas a dinero real. El checkout da un error, o el cliente paga y Stripe muestra el cobro pero tu app nunca le activa el plan, o el botón "Manage subscription" no hace nada.

Estos fallos tienen una lista corta de causas, y la mayoría están entre los dos modos de Stripe, no en tu código. Este es el orden en que los revisamos cuando una app de Lovable (o una de Bolt o v0 que usa el mismo patrón con Supabase) deja de cobrar después del lanzamiento.

Primero, ten claro qué estás depurando. Según la propia documentación de Lovable, la integración de Stripe hace las llamadas a Stripe desde edge functions, guarda la clave como un secreto del backend llamado STRIPE_SECRET_KEY y no usa webhooks por defecto: para las suscripciones, la app le pregunta a Stripe al iniciar sesión si el usuario tiene una suscripción activa, y identifica al usuario por su email. Si tú, o la IA, añadisteis webhooks después, te aplican las secciones 5 y 6. Si no, la causa está casi seguro en las secciones 1 a 4.

1. La app sigue con claves de test o price IDs de test

El modo test y el modo live de Stripe son mundos separados. Un producto que creaste en modo test no existe en modo live, y su price ID (price_...) es distinto. Los pasos de Lovable para salir a real son tres: meter una clave live en el formulario "Update Stripe secret key", cambiar cada price ID del código por el de live, y hacer una compra real en la app publicada.

El segundo paso es el que la gente se salta. El síntoma es un error de la función de checkout parecido a este:

No such price: 'price_1Q...'; a similar object exists in test mode,
but a live mode key was used to make this request.

Stripe te está diciendo exactamente qué ha pasado. Copia los price IDs de live del panel de Stripe (con el interruptor "Test mode" apagado) y sustituye los de test, estén donde los haya puesto la IA: un archivo de constantes, la edge function de checkout o filas de una tabla plans.

El error contrario sale más caro. La preview de Lovable no tiene un entorno de test separado, así que en cuanto guardas una clave live, cada compra que hagas probando en la preview es un cobro real. Devuélvete el dinero desde el panel y lleva las pruebas a una clave de test o a un proyecto aparte.

2. A la clave restringida le falta un permiso

Lovable acepta una clave restringida (rk_live_...) o una clave secreta (sk_live_...). Las restringidas son la opción más segura, pero la clave restringida de live se crea aparte de la de test, y es fácil darle menos permisos de los que necesita el código. Crear Checkout Sessions, leer customers y subscriptions, y crear sesiones del portal necesitan cada uno su propio permiso.

Síntoma: un error de Stripe que dice que la clave no tiene los permisos necesarios, visible en los logs de la edge function y en Stripe en Developers, luego Logs. Arreglo: edita la clave en Stripe y añade el permiso del recurso que falta. No hace falta una clave nueva para eso.

3. El cliente pagó con otro email

Es el que genera los tickets de soporte más confusos, porque no hay nada roto. El pago ha ido bien y la suscripción está activa en Stripe. La app sigue mostrando el plan gratuito.

La comprobación de suscripción por defecto de Lovable relaciona usuarios con customers de Stripe por dirección de email. Si el usuario se registró como ana@company.com y escribió ana.personal@gmail.com en Stripe Checkout, la comprobación no encuentra ninguna suscripción para ana@company.com. Pasa lo mismo cuando alguien cambia el email de acceso después de pagar.

Arreglo: haz que la función de checkout pase a Stripe el email del usuario con sesión (el parámetro customer_email, o un ID de customer ya existente) para que Checkout lo rellene y el usuario no pueda escribir otro. Mejor todavía: guarda el ID de customer de Stripe en la fila del perfil del usuario la primera vez que lo crees y busca las suscripciones por ese ID, no por email. Mete también el ID del usuario en client_reference_id o en los metadata, para poder relacionar siempre un pago con una cuenta.

4. El portal de cliente nunca se activó en modo live

El botón "Manage subscription" abre el portal de cliente alojado por Stripe, y el portal hay que configurarlo una vez en el panel de Stripe. Los modos test y live tienen configuraciones separadas, así que activarlo mientras probabas no se traslada. En modo live sin una configuración guardada, la llamada al portal falla con un error que dice que no se ha proporcionado configuración y que la configuración por defecto de live mode no se ha creado.

Arreglo: con el modo test apagado, abre Settings, luego Billing, luego Customer portal, elige qué pueden hacer los clientes (cancelar, cambiar de plan, actualizar tarjetas) y guarda. Si el botón sigue sin hacer nada, pruébalo en la app publicada en su propia pestaña: Lovable avisa de que el portal no funciona dentro del panel de la preview y de que Checkout se abre en una pestaña nueva, así que un bloqueador de popups se lo puede comer.

5. Webhooks: el endpoint solo existe en modo test

Si tu app depende de webhooks (por ejemplo, checkout.session.completed escribe is_pro = true en una tabla), el endpoint de webhook de live es un objeto aparte. Stripe no copia los endpoints de test a live. Cada endpoint tiene además su propio signing secret (whsec_...), así que el secreto de test rechazará todos los eventos de live.

Revisa, en orden:

  • En Stripe, modo live, Developers y luego Webhooks: ¿hay un endpoint que apunte a la URL de tu edge function, suscrito a los eventos que maneja tu código?
  • ¿El signing secret guardado en los secretos de tu backend es el de ese endpoint de live?
  • En las entregas de eventos del endpoint, ¿qué código de estado volvió? Es el diagnóstico más rápido de esta página. Un 401 apunta a la sección 6, un 400 a un problema de firma, un 500 a tu handler, y un 3xx a una URL que redirige (Stripe trata una redirección como una entrega fallida).

6. La edge function rechaza a Stripe antes de que se ejecute tu código

Hay dos problemas propios de los webhooks en edge functions de Supabase, que es lo que usan Lovable Cloud y la mayoría de proyectos de Lovable con su propio Supabase.

Un 401 en todas las entregas. Las edge functions de Supabase exigen por defecto un JWT válido en la cabecera Authorization. Stripe no lo manda; firma la petición con la cabecera Stripe-Signature. La función del webhook tiene que desplegarse con la verificación de JWT desactivada, y entonces la comprobación de la firma es lo único que hay entre internet y tu base de datos, así que tiene que estar. Con la CLI de Supabase:

supabase functions deploy stripe-webhook --no-verify-jwt

o, en supabase/config.toml:

[functions.stripe-webhook]
verify_jwt = false

Un 400 con un error de firma. Verificar la firma necesita el cuerpo de la petición en crudo, byte a byte. Si el handler llama antes a req.json(), la firma ya no coincide y te sale No signatures found matching the expected signature for payload. En Deno, el constructEvent síncrono también falla, porque Web Crypto es asíncrono. El ejemplo del propio Supabase usa esta forma:

const cryptoProvider = Stripe.createSubtleCryptoProvider()

const body = await req.text()
const event = await stripe.webhooks.constructEventAsync(
  body,
  req.headers.get('Stripe-Signature')!,
  Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
  undefined,
  cryptoProvider,
)

Una trampa más, propia de Lovable: un prompt posterior puede hacer que la IA regenere la función del webhook a partir de un patrón antiguo y vuelva a meter sin avisar req.json() o la llamada síncrona. Si los pagos se rompieron después de un cambio que no tenía nada que ver con los pagos, mira primero el diff de esa función.

7. La escritura en la base de datos falla en silencio

Llega el webhook, la firma cuadra, Stripe recibe su 200 y la fila del usuario sigue igual. Lo normal es que el handler escriba con un cliente que Row Level Security bloquea: un webhook no tiene usuario con sesión, así que una política como auth.uid() = user_id nunca puede cumplirse. El código de servidor que tiene que actualizar la fila de cualquier usuario necesita la clave service_role, leída de los secretos de la función y nunca enviada al navegador. Si ves new row violates row-level security policy en los logs de la función, nuestra guía de ese error de RLS explica el arreglo seguro.

Ya que estás en ese handler, revisa dos cosas más. Stripe reintenta las entregas, así que procesar el mismo evento dos veces no debe dar dos suscripciones ni mandar dos correos de bienvenida: guarda el ID del evento y sáltatelo si ya lo has visto. Y no desbloquees nada solo desde la página de éxito. La success URL es un enlace que puede abrir cualquiera; el acceso tiene que salir de lo que dice Stripe, a través de la comprobación al iniciar sesión o del webhook.

Dónde mirar si nada de esto encaja

Dos logs responden a casi todo lo que queda. Developers, luego Logs, en Stripe, muestra cada petición a la API que hicieron tus funciones y el error exacto que devolvió Stripe. Los logs de la edge function (en los logs de funciones de Lovable Cloud, o en el panel de Supabase en Edge Functions) muestran qué hizo tu código con esa respuesta. Leer los dos a la vez para una compra fallida suele cerrar la búsqueda.

Los pagos son la parte de una app donde un arreglo mal hecho cuesta dinero dos veces: una en ventas perdidas mientras está roto, y otra si el arreglo da acceso a quien no ha pagado. Si la IA lleva días parcheando el checkout y cada cambio mueve el problema a otro sitio, un diagnóstico con precio cerrado sale más barato: 450 €, 48 horas laborables y un informe por escrito de qué falla en el flujo de pago y en el resto de la app, con el precio de cada arreglo. Si sigues con el rescate (entre 1.500 € y 6.000 €, con precio cerrado por alcance), los 450 € se descuentan.

Si los pagos no son lo único que cambió entre la preview y el lanzamiento, empieza por la guía para migrar una app de Lovable a producción, y pasa el checklist de seguridad (en inglés) antes de que lleguen clientes reales. Las claves de Stripe y los handlers de webhooks están los dos en él.

Siguiente paso

¿Te suena este problema en tu proyecto?

Cuéntanoslo y te decimos cómo lo abordaríamos, en qué orden y con qué coste.