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.