[{"data":1,"prerenderedAt":1127},["ShallowReactive",2],{"blog-es-stripe-webhook-no-signatures-found":3,"blog-index-es":597},{"id":4,"title":5,"body":6,"category":554,"cover":555,"coverAlt":556,"description":557,"draft":558,"extension":559,"faq":560,"image":576,"keywords":577,"meta":585,"navigation":586,"ogTitle":587,"path":588,"published":589,"seo":590,"stem":591,"tldr":592,"updated":589,"__hash__":596},"blog_es\u002Fblog\u002Fstripe-webhook-no-signatures-found.md","\"No signatures found matching the expected signature\" en Supabase",{"type":7,"value":8,"toc":542},"minimark",[9,13,24,32,37,40,47,68,71,75,78,84,95,104,110,117,123,126,130,133,136,179,205,208,213,217,225,310,317,321,324,344,350,364,367,371,374,388,395,398,418,430,434,440,449,463,473,490,494,529],[10,11,12],"p",{},"Conectaste Stripe a tu app, un pago de prueba salió bien y el webhook que debía\nmarcar el pedido como pagado no hizo nada. El panel de Stripe muestra la entrega\nfallida, y los logs de tu Edge Function de Supabase dicen esto:",[14,15,20],"pre",{"className":16,"code":18,"language":19},[17],"language-text","No signatures found matching the expected signature for payload.\nAre you passing the raw request body you received from Stripe?\n","text",[21,22,18],"code",{"__ignoreMap":23},"",[10,25,26,27,31],{},"La buena noticia está en el origen del error. Lo lanza la parte de tu función\nque comprueba que la petición vino de verdad de Stripe, lo que significa que esa\ncomprobación existe y está haciendo su trabajo. ",[28,29,30],"strong",{},"\"No signatures found matching\nthe expected signature for payload\" tiene dos causas, y ninguna se arregla\ncomprobando menos."," El arreglo que aparece primero cuando alguien lo busca,\nenvolver la comprobación para que el error se ignore, es el que permite a\ncualquiera marcar un pedido como pagado.",[33,34,36],"h2",{"id":35},"qué-significa-no-signatures-found-matching-the-expected-signature","¿Qué significa \"No signatures found matching the expected signature\"?",[10,38,39],{},"Tu función comprobó la firma de Stripe en la petición, y no coincidió.",[10,41,42,43,46],{},"Piensa en cada webhook que envía Stripe como una carta cerrada con un sello de\nlacre. Stripe presiona el sello con un estampador que solo tienen Stripe y tú:\nel signing secret de tu endpoint, un valor que empieza por ",[21,44,45],{},"whsec_",". Cuando\nllega la carta, tu función presiona su propia copia del estampador contra el\nlacre. Si los dibujos encajan, la carta es de Stripe y nadie la ha tocado por el\ncamino.",[10,48,49,50,53,54,61,62,67],{},"El sello cubre la carta exactamente como se escribió. La librería de Stripe\ntoma el texto de la petición, la hora del encabezado ",[21,51,52],{},"Stripe-Signature"," y tu\nsecreto, calcula cómo debería ser la firma y la busca en ese encabezado. El\nmensaje de arriba está escrito\n",[55,56,60],"a",{"href":57,"rel":58},"https:\u002F\u002Fgithub.com\u002Fstripe\u002Fstripe-node\u002Fblob\u002Fmaster\u002Fsrc\u002FWebhooks.ts",[59],"nofollow","en el propio stripe-node",",\ny es lo que lanza cuando nada ahí coincide. La\n",[55,63,66],{"href":64,"rel":65},"https:\u002F\u002Fdocs.stripe.com\u002Fwebhooks\u002Fsignature",[59],"página de Stripe sobre errores de firma","\nlo dice sin rodeos: al menos uno de los tres valores que pasaste está mal.",[10,69,70],{},"Así que hay dos sitios donde mirar. O la carta se cambió antes de que tu función\ncomprobara el sello, o tu función tiene el estampador equivocado.",[33,72,74],{"id":73},"causa-1-el-cuerpo-cambió-antes-de-la-comprobación","Causa 1: el cuerpo cambió antes de la comprobación",[10,76,77],{},"La versión más común en una Edge Function es un cuerpo que primero se leyó como\nJSON y luego se volvió a convertir en texto para la comprobación.",[14,79,82],{"className":80,"code":81,"language":19},[17],"const event = await req.json()\nawait stripe.webhooks.constructEventAsync(JSON.stringify(event), signature, secret, undefined, cryptoProvider)\n",[21,83,81],{"__ignoreMap":23},[10,85,86,87,90,91,94],{},"Se lee con naturalidad y falla siempre. ",[21,88,89],{},"req.json()"," se queda con los datos y\ntira el texto: los espacios, los saltos de línea, el orden en que Stripe\nescribió las cosas. ",[21,92,93],{},"JSON.stringify"," escribe luego un texto nuevo con los mismos\ndatos dentro. El sello se presionó sobre el texto de Stripe, y esto es la copia\nde otra persona con otra letra, así que nunca puede coincidir.",[10,96,97,98,103],{},"Lo que hace el propio\n",[55,99,102],{"href":100,"rel":101},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fsupabase\u002Fblob\u002Fmaster\u002Fexamples\u002Fedge-functions\u002Fsupabase\u002Ffunctions\u002Fstripe-webhooks\u002Findex.ts",[59],"ejemplo de webhook de Stripe de Supabase","\nes leer el cuerpo una sola vez, como texto, y pasarlo sin tocar:",[14,105,108],{"className":106,"code":107,"language":19},[17],"const signature = req.headers.get('Stripe-Signature')\nconst body = await req.text()\nconst event = await stripe.webhooks.constructEventAsync(\n  body,\n  signature,\n  Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET'),\n  undefined,\n  cryptoProvider,\n)\n",[21,109,107],{"__ignoreMap":23},[10,111,112,113,116],{},"El comentario del ejemplo explica por qué: la verificación depende del cuerpo\ncrudo de la petición y no del JSON ya interpretado. No tienes que interpretar\nnada tú después, porque ",[21,114,115],{},"constructEventAsync"," devuelve el evento ya interpretado\nen cuanto el sello cuadra.",[118,119],"diagram",{"alt":120,"caption":121,"src":122},"Dos copias del mismo cuerpo de webhook, una al lado de la otra. La izquierda es el texto tal como llegó, y su sello encaja con el estampador, con una marca de visto. La derecha tiene los mismos valores reescritos en otras líneas, y el mismo estampador ya no encaja, tachado en amarillo.","La firma cubre el texto que envió Stripe, no los datos que contiene. Un texto reescrito con los mismos datos no pasa la comprobación.","\u002Fblog\u002Fstripe-webhook-no-signatures-found\u002Fsame-data-other-text-1600x720.png",[10,124,125],{},"Lo mismo ocurre fuera de tu código. La página de Stripe menciona frameworks que\ninterpretan o reformatean el cuerpo antes de que tu código lo vea, y el mensaje\nde stripe-node añade que una herramienta que te reenvía webhooks tiene que\nconservar el cuerpo exacto, incluidos el formato del JSON y el tipo de salto de\nlínea. Si hay un proxy o un servicio de reenvío entre Stripe y tu función,\ncomprueba que pase el cuerpo tal como llegó.",[33,127,129],{"id":128},"causa-2-el-signing-secret-no-es-el-correcto","Causa 2: el signing secret no es el correcto",[10,131,132],{},"Si el cuerpo está intacto, el estampador es el equivocado, y la página de Stripe\ndice que esta es la causa más común de todas.",[10,134,135],{},"Tres valores distintos pueden acabar en el sitio del signing secret, y se\nparecen:",[137,138,139,152,165],"ul",{},[140,141,142,145,146,151],"li",{},[28,143,144],{},"El secreto de otro endpoint."," La\n",[55,147,150],{"href":148,"rel":149},"https:\u002F\u002Fdocs.stripe.com\u002Fwebhooks",[59],"guía de webhooks de Stripe"," dice que cada\nendpoint recibe su propio secreto y que la misma dirección configurada en modo\nde prueba y en modo live tiene un secreto distinto en cada uno. Un pago live\ncomprobado con el secreto del endpoint de prueba falla, y al revés también.",[140,153,154,161,162,164],{},[28,155,156,157,160],{},"El secreto que imprime ",[21,158,159],{},"stripe listen","."," Cuando reenvías eventos a tu\nmáquina con la herramienta de línea de comandos de Stripe, imprime un secreto\n",[21,163,45],{}," propio. Stripe dice que es distinto del secreto del endpoint del\npanel y que es fácil confundirlos. Sirve para los eventos que reenvía la\nherramienta y para nada más.",[140,166,167,170,171,174,175,178],{},[28,168,169],{},"Tu clave API de Stripe."," ",[21,172,173],{},"sk_live_…"," o ",[21,176,177],{},"sk_test_…"," es la clave con la que\ntu función llama a Stripe. No es un signing secret, y una comprobación hecha\ncon ella nunca puede coincidir.",[10,180,181,182,185,186,189,190,193,194,197,198,201,202,160],{},"Para encontrar el correcto, abre el endpoint en el panel de Stripe en\n",[28,183,184],{},"Workbench"," y luego ",[28,187,188],{},"Webhooks",", y pulsa ",[28,191,192],{},"Reveal secret",". Antes, mira en\nqué modo estás. Después compáralo con el que lee tu función. En el ejemplo de\narriba es ",[21,195,196],{},"STRIPE_WEBHOOK_SIGNING_SECRET",", definido en la página\n",[28,199,200],{},"Edge Function Secrets"," de tu proyecto de Supabase o con\n",[21,203,204],{},"supabase secrets set",[10,206,207],{},"Ya que estás ahí, revisa una cosa más. Si el valor se pegó con un espacio o un\nsalto de línea al final, stripe-node añade una nota al mismo error:\n\"The provided signing secret contains whitespace.\" Borra el valor y vuelve a\npegarlo sin nada detrás.",[118,209],{"alt":210,"caption":211,"src":212},"Un sobre sellado a la izquierda y tres estampadores a la derecha, rotulados whsec_, stripe listen y sk_live_. Solo uno de los tres tiene el mismo dibujo que el sello y lleva una marca de visto. Los otros dos, el secreto que imprime stripe listen y una clave API sk_live_, tienen otros dibujos y están tachados en amarillo.","Cada endpoint, cada modo y la herramienta de línea de comandos firman con su propio secreto. Solo el que firmó este evento puede comprobarlo.","\u002Fblog\u002Fstripe-webhook-no-signatures-found\u002Fwhich-stamp-1600x720.png",[33,214,216],{"id":215},"los-mensajes-que-se-parecen-y-significan-otra-cosa","Los mensajes que se parecen y significan otra cosa",[10,218,219,220,224],{},"stripe-node lanza varios errores desde la misma comprobación, y cada uno apunta a\nun problema distinto. Todos están escritos\n",[55,221,223],{"href":57,"rel":222},[59],"en su código de webhooks",":",[226,227,228,241],"table",{},[229,230,231],"thead",{},[232,233,234,238],"tr",{},[235,236,237],"th",{},"Lo que dicen tus logs",[235,239,240],{},"Lo que pasó",[242,243,244,255,271,284,294],"tbody",{},[232,245,246,252],{},[247,248,249],"td",{},[21,250,251],{},"No signatures found matching the expected signature for payload",[247,253,254],{},"El cuerpo cambió, o el secreto no es el correcto. Este artículo.",[232,256,257,262],{},[247,258,259],{},[21,260,261],{},"Webhook payload must be provided as a string or a Buffer",[247,263,264,265,267,268,160],{},"Pasaste el objeto interpretado de ",[21,266,89],{},". Pasa el texto de ",[21,269,270],{},"req.text()",[232,272,273,278],{},[247,274,275],{},[21,276,277],{},"No stripe-signature header value was provided.",[247,279,280,281,283],{},"La petición no traía el encabezado ",[21,282,52],{},". Puede que ni siquiera viniera de Stripe.",[232,285,286,291],{},[247,287,288],{},[21,289,290],{},"Timestamp outside the tolerance zone",[247,292,293],{},"El sello coincidió, pero el evento es más antiguo que el límite. Las librerías de Stripe usan 5 minutos.",[232,295,296,301],{},[247,297,298],{},[21,299,300],{},"SubtleCryptoProvider cannot be used in a synchronous context.",[247,302,303,304,307,308,160],{},"Llamaste a ",[21,305,306],{},"constructEvent",". Con el proveedor Web Crypto que necesita Deno, usa ",[21,309,115],{},[10,311,312,313,316],{},"Y uno que nunca llega a tu código. Si el panel de Stripe muestra la entrega\nfallida con un ",[28,314,315],{},"401"," y los logs de tu función están vacíos, la plataforma de\nSupabase rechazó la petición antes de que tu función corriera, por una\ncomprobación que en un webhook tiene que estar desactivada.",[33,318,320],{"id":319},"por-qué-tu-webhook-necesita-verify_jwt-desactivado","Por qué tu webhook necesita verify_jwt desactivado",[10,322,323],{},"Porque Stripe no puede demostrar quién es como lo hacen los usuarios de tu app,\ny con la comprobación activada cada entrega se rechaza.",[10,325,326,327,330,331,334,335,340,341,224],{},"Supabase pone delante de cada Edge Function una comprobación llamada\n",[21,328,329],{},"verify_jwt",", activada por defecto. Pide un token de Supabase en el encabezado\n",[21,332,333],{},"Authorization"," de la petición, y Stripe no tiene ninguno que enviar. La\n",[55,336,339],{"href":337,"rel":338},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Ffunctions\u002Ffunction-configuration",[59],"guía de configuración de funciones","\nde Supabase usa justo esto como ejemplo, diciendo que los webhooks de Stripe\ntienen que ser accesibles públicamente, y desactiva la comprobación para esa\núnica función en ",[21,342,343],{},"supabase\u002Fconfig.toml",[14,345,348],{"className":346,"code":347,"language":19},[17],"[functions.stripe-webhook]\nverify_jwt = false\n",[21,349,347],{"__ignoreMap":23},[10,351,352,353,356,357,360,361,160],{},"El comando ",[21,354,355],{},"supabase functions deploy"," acepta ",[21,358,359],{},"--no-verify-jwt"," para lo mismo.\nEl ejemplo de Stripe de Supabase lleva la misma instrucción en un comentario:\nStripe verifica la petición mediante su firma, así que despliégalo con\n",[21,362,363],{},"verify_jwt = false",[10,365,366],{},"Con la comprobación de la plataforma desactivada, cualquier petición de internet\nllega a tu código, y la comprobación del sello dentro de tu función es lo único\nque distingue las cartas de Stripe de las de cualquier otro.",[33,368,370],{"id":369},"por-qué-nunca-deberías-saltarte-la-comprobación-de-firma","Por qué nunca deberías saltarte la comprobación de firma",[10,372,373],{},"Porque sin ella, cualquiera puede decirle a tu app que un pago se completó.",[10,375,376,377,380,381,384,385,387],{},"La dirección de una función de webhook es la URL de tu proyecto, que está en tu\nfrontend para quien quiera leerla, seguida de ",[21,378,379],{},"\u002Ffunctions\u002Fv1\u002F"," y del nombre de\nla función, que suele ser el obvio. El ejemplo de la guía de configuración la\nllama ",[21,382,383],{},"stripe-webhook",". Con ",[21,386,329],{}," desactivado, la plataforma deja pasar\ncada petición hasta tu código. Si tu código ya no comprueba el sello, se cree\ncada carta que recibe.",[10,389,390,391,394],{},"Así que un desconocido envía a tu función una petición que dice\n",[21,392,393],{},"checkout.session.completed",", con un pedido o un cliente elegido por él. Tu\nfunción hace lo que hace con una real: marca el pedido como pagado, sube de plan\nla cuenta, desbloquea la descarga. Nadie pagó. Stripe no tiene registro de ello,\nporque Stripe nunca lo envió.",[10,396,397],{},"Tres formas de saltarse la comprobación terminan ahí:",[137,399,400,406,412],{},[140,401,402,405],{},[28,403,404],{},"Borrarla"," porque no pasaba.",[140,407,408,411],{},[28,409,410],{},"Capturar el error y seguir",", registrando el fallo y procesando el evento de\ntodos modos. El ejemplo devuelve un 400 desde su bloque catch, y esa es la\nforma que hay que mantener: una petición que no pasa la comprobación se queda\nahí.",[140,413,414,417],{},[28,415,416],{},"Poner la tolerancia de tiempo en 0."," La guía de webhooks de Stripe dice\nque no se haga: 0 desactiva por completo la comprobación de antigüedad, así\nque un evento real que alguien capturó puede volver a enviarse más tarde.",[10,419,420,421,425,426,160],{},"Una clave secreta de Stripe activa en tu frontend es la otra forma en que los\npagos salen mal, y es la que podemos ver desde fuera: encontramos una en\n",[55,422,424],{"href":423},"\u002Fblog\u002Fstripe-secret-key-in-frontend","3 de 30.998 apps escaneadas",". Nuestro\nescaneo gratuito lee el JavaScript de tu app publicada como lo haría un\ndesconocido y nombra cada clave que encuentra, indicando si es de las que deben\nestar ahí. Tarda unos 20 segundos y no necesita cuenta:\n",[55,427,429],{"href":428},"\u002Fsecurity-scanner","escanea tu app",[33,431,433],{"id":432},"si-alguna-vez-se-cuela-un-pago-falso","Si alguna vez se cuela un pago falso",[10,435,436,439],{},[28,437,438],{},"Una comprobación que quitó un prompt no se anuncia."," Un builder al que le\npides que el webhook vuelva a funcionar puede tomar el atajo de arriba, y tu app\nsigue funcionando mientras tanto.",[10,441,442],{},[28,443,444,448],{},[55,445,447],{"href":446},"\u002Fwebsite-monitoring","Reeve Monitor"," vigila lo que un desconocido puede ver desde fuera:",[137,450,451,454,457,460],{},[140,452,453],{},"las nueve comprobaciones cada hora, en hasta tres apps",[140,455,456],{},"un correo el día en que un nuevo escaneo califica tu app peor que el anterior; una clave secreta en tu JavaScript limita por sí sola la nota a D",[140,458,459],{},"si la app está disponible, cada 60 segundos",[140,461,462],{},"un informe mensual de lo que vio",[10,464,465],{},[28,466,467,468,472],{},"Un evento falso escribe en tu base de datos como uno real.\n",[55,469,471],{"href":470},"\u002Fsupabase-backups","Reeve Care"," guarda una copia de tu base de datos de Supabase\nde antes de que pasara.",[137,474,475,478,481,484,487],{},[140,476,477],{},"una copia cifrada según un calendario, guardada en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase",[140,479,480],{},"cada copia verificada antes de contar, contando las filas de cada tabla",[140,482,483],{},"una restauración en un clic que guarda lo que hay ahora antes de reemplazarlo",[140,485,486],{},"también tus archivos subidos, en cuanto conectes una credencial de Storage",[140,488,489],{},"todo lo que hace Monitor",[33,491,493],{"id":492},"qué-hacer-ahora","Qué hacer ahora",[495,496,497],"key-takeaways",{},[137,498,499,502,514,520,526],{},[140,500,501],{},"Abre la entrega fallida en el panel de Stripe y la pestaña Logs de tu función, y lee el mensaje exacto antes de cambiar nada.",[140,503,504,505,508,509,511,512,160],{},"Lee el cuerpo con ",[21,506,507],{},"await req.text()"," y pasa ese texto a ",[21,510,115],{}," sin tocarlo. Nunca compruebes un evento pasado por ",[21,513,93],{},[140,515,516,517,519],{},"Copia el secreto ",[21,518,45],{}," del endpoint desde el que envía Stripe, en el modo correcto, y pégalo en Edge Function Secrets sin nada detrás.",[140,521,522,523,525],{},"Mantén ",[21,524,363],{}," en el webhook, y la comprobación de firma como lo primero que hace la función.",[140,527,528],{},"Devuelve un 400 cuando la comprobación falle. Nunca captures el error para procesar el evento igualmente.",[10,530,531,532,536,537,541],{},"Lo demás que necesita una función que guarda un secreto está en\n",[55,533,535],{"href":534},"\u002Fblog\u002Fmove-a-secret-to-an-edge-function","mover un secreto a una Edge Function",",\ny si tu error es el genérico de supabase-js,\n",[55,538,540],{"href":539},"\u002Fblog\u002Fedge-function-returned-a-non-2xx-status-code","leer un código de estado non-2xx","\nexplica dónde está el mensaje real.",{"title":23,"searchDepth":543,"depth":543,"links":544},3,[545,547,548,549,550,551,552,553],{"id":35,"depth":546,"text":36},2,{"id":73,"depth":546,"text":74},{"id":128,"depth":546,"text":129},{"id":215,"depth":546,"text":216},{"id":319,"depth":546,"text":320},{"id":369,"depth":546,"text":370},{"id":432,"depth":546,"text":433},{"id":492,"depth":546,"text":493},"Fundamentos de seguridad","\u002Fblog\u002Fstripe-webhook-no-signatures-found\u002Fcover-1200x630.png","Un sobre cerrado con un sello de lacre turquesa y, al lado, un estampador cuyo dibujo no coincide con la marca, señalado en amarillo.","¿Tu webhook de Stripe en una Edge Function de Supabase dice \"No signatures found matching the expected signature\"? Tiene dos causas. Sigue verificando.",false,"md",[561,564,567,570,573],{"q":562,"a":563},"¿Qué significa \"No signatures found matching the expected signature for payload\"?","Tu función de webhook calculó la firma que esperaba a partir del cuerpo de la petición y de tu signing secret, y ninguna de las firmas del encabezado Stripe-Signature coincidió. La librería de Stripe lanza este error cuando el cuerpo que recibe no es, byte a byte, lo que Stripe envió, o cuando el secreto no es el del endpoint que envió el evento.",{"q":565,"a":566},"¿Por qué falla si paso JSON.stringify del evento?","Porque la firma cubre el texto exacto que envió Stripe, con sus espacios y saltos de línea. Si lees el cuerpo con req.json() y lo vuelves a convertir en texto con JSON.stringify, tienes los mismos datos en otro texto, y otro texto tiene otra firma. Lee el cuerpo una vez con await req.text() y pasa ese texto a constructEventAsync.",{"q":568,"a":569},"¿Qué signing secret uso?","El secreto whsec_ del endpoint exacto al que Stripe entrega. Cada endpoint tiene el suyo, la misma dirección configurada en modo de prueba y en modo live tiene un secreto distinto en cada uno, y el comando stripe listen imprime un secreto propio que solo sirve para los eventos que él reenvía. Tu clave API sk_ no es un signing secret.",{"q":571,"a":572},"¿Mi Edge Function de webhook de Stripe debe tener verify_jwt desactivado?","Sí. Stripe no tiene un token de Supabase que enviar, así que con la comprobación activada cada entrega se rechaza con un 401 antes de que tu código corra. El propio ejemplo de Stripe de Supabase dice que se despliegue con verify_jwt = false. Eso convierte la comprobación de firma dentro de tu función en la única que hay, así que nunca debe saltarse ni capturarse e ignorarse.",{"q":574,"a":575},"¿Puedo quitar la comprobación de firma para que funcionen los pagos?","Puedes, y entonces cualquiera que encuentre la dirección de la función podrá enviarle un evento que diga que un pago se completó. La dirección es la URL de tu proyecto, que está en tu frontend, más el nombre de la función. Sin la comprobación, tu función se cree cada petición que recibe y concede lo que el evento diga que se pagó.","\u002Fblog\u002Fstripe-webhook-no-signatures-found\u002Fcard-800x500.png",[578,579,580,581,582,583,584],"no signatures found matching the expected signature for payload","stripe webhook supabase edge function","stripe signature verification failed","constructEventAsync deno","stripe webhook raw body","stripe webhook signing secret","supabase stripe webhook 401",{},true,"\"No signatures found matching the expected signature\"","\u002Fblog\u002Fstripe-webhook-no-signatures-found","2026-10-29",{"title":5,"description":557},"blog\u002Fstripe-webhook-no-signatures-found",[593,594,595],"\"No signatures found matching the expected signature for payload\" significa que tu función comprobó la firma de Stripe y no coincidió. O el cuerpo cambió antes de la comprobación, o el signing secret no es el que Stripe usó para firmar.","Lee el cuerpo con await req.text() y pasa ese texto, sin tocarlo, a constructEventAsync. Después compara el secreto whsec_ que tiene tu función con el del endpoint exacto desde el que envía Stripe.","No hagas desaparecer el error saltándote la comprobación. El webhook tiene que funcionar con verify_jwt = false, así que la firma es lo único que impide que un desconocido le diga a tu app que un pago se completó.","ij9OV0z-xsxC952URqzfjANsMIv-v7pkQkCt_nCyhTg",[598,604,610,616,622,628,634,640,646,652,658,665,666,672,677,683,689,695,701,707,713,719,725,731,737,743,749,755,761,767,773,779,785,791,797,803,808,814,820,826,832,838,844,850,856,862,867,873,879,885,891,897,903,908,914,920,926,932,938,944,950,956,962,968,974,980,986,992,998,1004,1010,1016,1022,1028,1034,1040,1046,1052,1058,1064,1070,1076,1082,1088,1093,1099,1105,1111,1116,1122],{"path":599,"title":600,"description":601,"published":602,"category":554,"image":603,"draft":558},"\u002Fblog\u002Fmax-client-connections-reached","Supabase \"max client connections reached\", explicado","Supabase \"max client connections reached\": el pooler no tiene sitio para un cliente más. El puerto de tu cadena de conexión decide el límite.","2026-11-09","\u002Fblog\u002Fmax-client-connections-reached\u002Fcard-800x500.png",{"path":605,"title":606,"description":607,"published":608,"category":554,"image":609,"draft":558},"\u002Fblog\u002Funsupported-provider-provider-is-not-enabled","Error de Supabase: \"Unsupported provider: provider is not enabled\"","\"Unsupported provider: provider is not enabled\": Google está desactivado en tu proyecto Supabase. Cómo activarlo y arreglar redirect_uri_mismatch.","2026-11-08","\u002Fblog\u002Funsupported-provider-provider-is-not-enabled\u002Fcard-800x500.png",{"path":611,"title":612,"description":613,"published":614,"category":554,"image":615,"draft":558},"\u002Fblog\u002Fviolates-foreign-key-constraint","Error de Supabase \"violates foreign key constraint\" al insertar","Supabase \"insert or update on table violates foreign key constraint\": tu fila apunta a una fila que no existe. Cuál, por qué, y cómo arreglarlo.","2026-11-07","\u002Fblog\u002Fviolates-foreign-key-constraint\u002Fcard-800x500.png",{"path":617,"title":618,"description":619,"published":620,"category":554,"image":621,"draft":558},"\u002Fblog\u002Fnull-value-violates-not-null-constraint","Error de Supabase \"null value in column violates not-null constraint\"","Supabase \"null value in column violates not-null constraint\": una columna obligatoria llegó vacía. De dónde vino el null, y cómo arreglarlo.","2026-11-06","\u002Fblog\u002Fnull-value-violates-not-null-constraint\u002Fcard-800x500.png",{"path":623,"title":624,"description":625,"published":626,"category":554,"image":627,"draft":558},"\u002Fblog\u002Fauth-session-missing","Supabase \"Auth session missing!\": adónde fue la sesión","\"Auth session missing!\" es supabase-js sin encontrar ninguna sesión iniciada y sin llegar a preguntar a Supabase. Adónde fue la sesión, y el arreglo.","2026-11-05","\u002Fblog\u002Fauth-session-missing\u002Fcard-800x500.png",{"path":629,"title":630,"description":631,"published":632,"category":554,"image":633,"draft":558},"\u002Fblog\u002Finvalid-claim-missing-sub-claim","Supabase \"invalid claim: missing sub claim\": no hay nadie conectado","\"invalid claim: missing sub claim\": Supabase Auth recibió un token auténtico sin ningún usuario, la clave del proyecto en lugar de una sesión.","2026-11-04","\u002Fblog\u002Finvalid-claim-missing-sub-claim\u002Fcard-800x500.png",{"path":635,"title":636,"description":637,"published":638,"category":554,"image":639,"draft":558},"\u002Fblog\u002Ffailed-to-send-a-request-to-the-edge-function","\"Failed to send a request to the Edge Function\": la causa real","\"Failed to send a request to the Edge Function\": el navegador no recibió una respuesta legible. Su consola dice por qué y si tu función se ejecutó.","2026-11-03","\u002Fblog\u002Ffailed-to-send-a-request-to-the-edge-function\u002Fcard-800x500.png",{"path":641,"title":642,"description":643,"published":644,"category":554,"image":645,"draft":558},"\u002Fblog\u002Fsupabase-auth-redirects-to-localhost","Por qué los enlaces de email de Supabase redirigen a localhost:3000","Si Supabase redirige a localhost:3000, la Site URL nunca se cambió o tu dirección falta en Redirect URLs. La solución, y el comodín que debes evitar.","2026-11-02","\u002Fblog\u002Fsupabase-auth-redirects-to-localhost\u002Fcard-800x500.png",{"path":647,"title":648,"description":649,"published":650,"category":554,"image":651,"draft":558},"\u002Fblog\u002Femail-link-is-invalid-or-has-expired","Supabase \"Email link is invalid or has expired\" con un enlace nuevo","Con un enlace enviado hace un minuto, \"Email link is invalid or has expired\" en Supabase suele indicar que un filtro de correo lo usó antes. Cómo arreglarlo.","2026-11-01","\u002Fblog\u002Femail-link-is-invalid-or-has-expired\u002Fcard-800x500.png",{"path":653,"title":654,"description":655,"published":656,"category":554,"image":657,"draft":558},"\u002Fblog\u002Fcanceling-statement-due-to-statement-timeout","Supabase \"canceling statement due to statement timeout\", explicado","Supabase \"canceling statement due to statement timeout\": una consulta superó el límite de su rol, 3 segundos para un visitante. Revisa antes la política RLS.","2026-10-31","\u002Fblog\u002Fcanceling-statement-due-to-statement-timeout\u002Fcard-800x500.png",{"path":659,"title":660,"description":661,"published":662,"category":663,"image":664,"draft":558},"\u002Fblog\u002Fdatabase-error-deleting-user","\"Database error deleting user\" en Supabase, y qué se va con él","\"Database error deleting user\" en Supabase: tu base de datos rechazó el borrado. La causa está en tus logs y el arreglo decide qué más se borra.","2026-10-30","Backups","\u002Fblog\u002Fdatabase-error-deleting-user\u002Fcard-800x500.png",{"path":588,"title":5,"description":557,"published":589,"category":554,"image":576,"draft":558},{"path":667,"title":668,"description":669,"published":670,"category":554,"image":671,"draft":558},"\u002Fblog\u002Fsupabase-invalid-login","\"Invalid login credentials\" en Supabase: las tres causas","Supabase dice \"Invalid login credentials\" ante una contraseña errónea, un email desconocido y una cuenta de Google. Cómo saber cuál es.","2026-10-28","\u002Fblog\u002Fsupabase-invalid-login\u002Fcard-800x500.png",{"path":539,"title":673,"description":674,"published":675,"category":554,"image":676,"draft":558},"\"Edge Function returned a non-2xx status code\": el error real","\"Edge Function returned a non-2xx status code\" significa que tu función de Supabase respondió con un error. Aquí ves dónde leer el real.","2026-10-27","\u002Fblog\u002Fedge-function-returned-a-non-2xx-status-code\u002Fcard-800x500.png",{"path":678,"title":679,"description":680,"published":681,"category":554,"image":682,"draft":558},"\u002Fblog\u002Fcertificate-not-trusted","Certificado no confiable: tres causas aparte de la caducidad","¿Certificado no confiable aunque la fecha está bien? Nombra otra dirección, se firmó a sí mismo o a su cadena le falta un eslabón. Cómo saber cuál.","2026-10-26","\u002Fblog\u002Fcertificate-not-trusted\u002Fcard-800x500.png",{"path":684,"title":685,"description":686,"published":687,"category":554,"image":688,"draft":558},"\u002Fblog\u002Finvalid-input-syntax-for-type-uuid","Supabase \"invalid input syntax for type uuid\": lee las comillas","Supabase \"invalid input syntax for type uuid\" significa que tu app envió algo que no es un id. Lee las comillas, frena la consulta, conserva la columna.","2026-10-25","\u002Fblog\u002Finvalid-input-syntax-for-type-uuid\u002Fcard-800x500.png",{"path":690,"title":691,"description":692,"published":693,"category":554,"image":694,"draft":558},"\u002Fblog\u002Fduplicate-key-value-violates-unique-constraint","Supabase \"duplicate key value violates unique constraint\", explicado","Supabase \"duplicate key value violates unique constraint\": el valor ya está ocupado. Encuentra la columna, elige un arreglo y aparta el valor de la pantalla.","2026-10-24","\u002Fblog\u002Fduplicate-key-value-violates-unique-constraint\u002Fcard-800x500.png",{"path":696,"title":697,"description":698,"published":699,"category":554,"image":700,"draft":558},"\u002Fblog\u002Fsupabase-relation-does-not-exist","Supabase \"relation does not exist\" aunque la tabla está ahí","Supabase \"relation does not exist\" o PGRST205: ninguna tabla tiene ese nombre en ese esquema. Cuatro comprobaciones, y lo que abre exponer un esquema.","2026-10-23","\u002Fblog\u002Fsupabase-relation-does-not-exist\u002Fcard-800x500.png",{"path":702,"title":703,"description":704,"published":705,"category":554,"image":706,"draft":558},"\u002Fblog\u002Fsupabase-function-not-found","Supabase PGRST202: \"Could not find the function in the schema cache\"","Supabase \"Could not find the function in the schema cache\" muestra los nombres de argumentos que envió tu app. Compáralos con la función.","2026-10-22","\u002Fblog\u002Fsupabase-function-not-found\u002Fcard-800x500.png",{"path":708,"title":709,"description":710,"published":711,"category":554,"image":712,"draft":558},"\u002Fblog\u002Fsupabase-schema-cache-column-error","Supabase PGRST204: \"Could not find the column in the schema cache\"","Supabase \"Could not find the column in the schema cache\" suele ser un nombre que no coincide. Cuatro comprobaciones y la línea que recarga.","2026-10-21","\u002Fblog\u002Fsupabase-schema-cache-column-error\u002Fcard-800x500.png",{"path":714,"title":715,"description":716,"published":717,"category":554,"image":718,"draft":558},"\u002Fblog\u002Fsupabase-email-rate-limit-exceeded","Supabase \"Email Rate Limit Exceeded\": dos límites, un solo código","Supabase \"Email Rate Limit Exceeded\" limita cada email que envía tu proyecto: 2 por hora con el remitente integrado. Qué cuenta y cómo subirlo.","2026-10-20","\u002Fblog\u002Fsupabase-email-rate-limit-exceeded\u002Fcard-800x500.png",{"path":720,"title":721,"description":722,"published":723,"category":554,"image":724,"draft":558},"\u002Fblog\u002Fsupabase-users-logged-out","Supabase \"Invalid Refresh Token: Refresh Token Not Found\": 3 causas","Supabase \"Invalid Refresh Token: Refresh Token Not Found\" cierra la sesión de tus usuarios. Tres causas, y cómo las separa el log de Auth.","2026-10-19","\u002Fblog\u002Fsupabase-users-logged-out\u002Fcard-800x500.png",{"path":726,"title":727,"description":728,"published":729,"category":554,"image":730,"draft":558},"\u002Fblog\u002Fpermission-denied-for-schema-public","Supabase \"permission denied for schema public\": tres causas","Supabase \"permission denied for schema public\" es un grant que falta en el esquema. Qué lo quitó y la línea que lo devuelve.","2026-10-18","\u002Fblog\u002Fpermission-denied-for-schema-public\u002Fcard-800x500.png",{"path":732,"title":733,"description":734,"published":735,"category":554,"image":736,"draft":558},"\u002Fblog\u002Fsupabase-bucket-not-found","Supabase \"Bucket not found\": las tres cosas que significa","Supabase \"Bucket not found\" es una de tres cosas: un enlace público a un bucket privado, un nombre que no coincide exactamente o el proyecto equivocado.","2026-10-17","\u002Fblog\u002Fsupabase-bucket-not-found\u002Fcard-800x500.png",{"path":738,"title":739,"description":740,"published":741,"category":554,"image":742,"draft":558},"\u002Fblog\u002Fsupabase-invalid-jwt","Supabase invalid JWT: ¿qué clave dejó de funcionar?","Un error Supabase invalid JWT es PGRST301: un token firmado no pasó la verificación. En un proyecto nuevo se trata de la sesión, no de la clave de API.","2026-10-16","\u002Fblog\u002Fsupabase-invalid-jwt\u002Fcard-800x500.png",{"path":744,"title":745,"description":746,"published":747,"category":554,"image":748,"draft":558},"\u002Fblog\u002Fsupabase-single-row-error","Supabase \"multiple (or no) rows returned\" casi siempre es ninguna","Supabase devuelve PGRST116, \"JSON object requested, multiple (or no) rows returned\", cuando tu consulta no encontró nada. Una fila oculta parece ausente.","2026-10-15","\u002Fblog\u002Fsupabase-single-row-error\u002Fcard-800x500.png",{"path":750,"title":751,"description":752,"published":753,"category":554,"image":754,"draft":558},"\u002Fblog\u002Fdatabase-error-saving-new-user","\"Database error saving new user\" en Supabase: dónde mirar","\"Database error saving new user\" significa que algo unido a tu registro de Supabase falló y se deshizo entero. El error real está en tus logs.","2026-10-14","\u002Fblog\u002Fdatabase-error-saving-new-user\u002Fcard-800x500.png",{"path":756,"title":757,"description":758,"published":759,"category":554,"image":760,"draft":558},"\u002Fblog\u002Fwhat-is-supabase","¿Qué es Supabase? La base de datos que eligió tu builder","Qué es Supabase: la base de datos, el login y el almacenamiento de archivos de tu app. Supabase lleva los servidores; las reglas sobre tus filas son tuyas.","2026-10-13","\u002Fblog\u002Fwhat-is-supabase\u002Fcard-800x500.png",{"path":762,"title":763,"description":764,"published":765,"category":554,"image":766,"draft":558},"\u002Fblog\u002Faws-access-key-exposed-in-frontend","Una clave AWS expuesta en tu frontend no es como una clave OpenAI","Una clave de acceso de AWS expuesta en tu frontend lee y escribe tu almacenamiento. La solución suele ser una presigned URL, no una clave nueva.","2026-10-12","\u002Fblog\u002Faws-access-key-exposed-in-frontend\u002Fcard-800x500.png",{"path":768,"title":769,"description":770,"published":771,"category":554,"image":772,"draft":558},"\u002Fblog\u002Fstorage-upload-violates-row-level-security","\"Row-level security policy for table objects\" al subir","\"New row violates row-level security policy for table objects\" significa que tu subida no tiene regla insert. Hacer público el bucket no añade ninguna.","2026-10-11","\u002Fblog\u002Fstorage-upload-violates-row-level-security\u002Fcard-800x500.png",{"path":774,"title":775,"description":776,"published":777,"category":554,"image":778,"draft":558},"\u002Fblog\u002Fbase44-security-scan","Escaneo de seguridad de Base44: lo que solo él ve","El escaneo de seguridad de Base44 revisa siete tipos de problema desde dentro. Esta es la mitad que lee y nadie más puede leer, y la mitad que nunca mira.","2026-10-10","\u002Fblog\u002Fbase44-security-scan\u002Fcard-800x500.png",{"path":780,"title":781,"description":782,"published":783,"category":554,"image":784,"draft":558},"\u002Fblog\u002Fapi-key-leaked-what-to-do","Tu clave API se filtró. El orden en el que hay que actuar","Una clave API se filtró y no sabes por dónde empezar. No todas las claves de tu frontend lo son, y el orden importa más que la velocidad.","2026-10-09","\u002Fblog\u002Fapi-key-leaked-what-to-do\u002Fcard-800x500.png",{"path":786,"title":787,"description":788,"published":789,"category":554,"image":790,"draft":558},"\u002Fblog\u002Finfinite-recursion-in-policy-for-relation","\"Infinite recursion detected in policy\" sin desactivar RLS","\"Infinite recursion detected in policy for relation\" significa que tu policy preguntó a la tabla que protege. Así se rompe el círculo.","2026-10-08","\u002Fblog\u002Finfinite-recursion-in-policy-for-relation\u002Fcard-800x500.png",{"path":792,"title":793,"description":794,"published":795,"category":554,"image":796,"draft":558},"\u002Fblog\u002Fno-api-key-found-in-request","\"No API key found in request\" en Supabase, y el arreglo equivocado","\"No API key found in request\" significa que tu petición llegó a Supabase sin clave. Casi todas las respuestas apuntan a las reglas de tu base de datos.","2026-10-07","\u002Fblog\u002Fno-api-key-found-in-request\u002Fcard-800x500.png",{"path":798,"title":799,"description":800,"published":801,"category":554,"image":802,"draft":558},"\u002Fblog\u002Fis-base44-safe","¿Es seguro Base44? Lo que vimos en 5.442 apps Base44","¿Es seguro Base44? Pasamos nueve comprobaciones a 5.442 apps Base44 en producción. Casi todas sacaron B por motivos que su dueño nunca eligió.","2026-10-06","\u002Fblog\u002Fis-base44-safe\u002Fcard-800x500.png",{"path":534,"title":804,"description":805,"published":806,"category":554,"image":807,"draft":558},"Ocultar una clave API: muévela a una Edge Function de Supabase","Ocultar una clave API significa sacarla del navegador, y una Edge Function de Supabase es el sitio más pequeño donde ponerla. Dos pasos importan más.","2026-10-05","\u002Fblog\u002Fmove-a-secret-to-an-edge-function\u002Fcard-800x500.png",{"path":809,"title":810,"description":811,"published":812,"category":554,"image":813,"draft":558},"\u002Fblog\u002Fenv-file-exposed-on-your-server","¿Tu archivo .env está expuesto? Las doce rutas a revisar","¿Tu archivo .env está expuesto en tu propio servidor web? Doce direcciones te lo dicen, y un acierto significa que todo lo que contiene ya es público.","2026-10-04","\u002Fblog\u002Fenv-file-exposed-on-your-server\u002Fcard-800x500.png",{"path":815,"title":816,"description":817,"published":818,"category":554,"image":819,"draft":558},"\u002Fblog\u002Fis-v0-safe","Seguridad de v0: las 1.790 aplicaciones escaneadas sacaron una A","Seguridad de v0, medida en 1.790 aplicaciones v0 en vivo: todas sacaron A. Solo 17 nombraban una base de datos, y eso es casi todo lo que mide la A.","2026-10-03","\u002Fblog\u002Fis-v0-safe\u002Fcard-800x500.png",{"path":821,"title":822,"description":823,"published":824,"category":554,"image":825,"draft":558},"\u002Fblog\u002Fis-bolt-safe","¿Es seguro Bolt? Lo que vimos en 1.123 aplicaciones Bolt","¿Es seguro Bolt? Pasamos 1.123 aplicaciones Bolt en vivo por nueve comprobaciones. El hosting salió limpio. Los hallazgos eran claves y tablas abiertas dentro.","2026-10-02","\u002Fblog\u002Fis-bolt-safe\u002Fcard-800x500.png",{"path":827,"title":828,"description":829,"published":830,"category":554,"image":831,"draft":558},"\u002Fblog\u002Fis-supabase-down-or-your-app","¿Supabase está caído o es tu app? Cómo distinguirlo","¿Supabase está caído o es tu app? Un proyecto pausado, una base de datos llena y un fallo tuyo parecen lo mismo. Cuatro comprobaciones los separan.","2026-10-01","\u002Fblog\u002Fis-supabase-down-or-your-app\u002Fcard-800x500.png",{"path":833,"title":834,"description":835,"published":836,"category":554,"image":837,"draft":558},"\u002Fblog\u002Fdoes-supabase-encrypt-my-data","¿Supabase cifra mis datos? Sí. Esto es lo que detiene","¿Supabase cifra los datos? Sí: AES-256 en reposo, TLS en tránsito, SOC 2 e ISO 27001. Qué cubre cada uno, y la fuga que ninguno de ellos detiene.","2026-09-30","\u002Fblog\u002Fdoes-supabase-encrypt-my-data\u002Fcard-800x500.png",{"path":839,"title":840,"description":841,"published":842,"category":554,"image":843,"draft":558},"\u002Fblog\u002Fsupabase-free-plan-limits","¿Cuánto cuesta Supabase? Los límites del plan gratuito","Los límites del plan gratuito de Supabase: 500 MB de base de datos, 5 GB de egress y ningún backup. Qué pasa en cada uno y qué cambia Pro por $25.","2026-09-29","\u002Fblog\u002Fsupabase-free-plan-limits\u002Fcard-800x500.png",{"path":845,"title":846,"description":847,"published":848,"category":663,"image":849,"draft":558},"\u002Fblog\u002Ftest-your-supabase-backup","Prueba tu backup de Supabase antes del día que lo necesites","Cómo probar tu backup de Supabase: restaurarlo en un proyecto de prueba, contar filas, iniciar sesión y buscar la línea que le falta a un archivo cortado.","2026-09-28","\u002Fblog\u002Ftest-your-supabase-backup\u002Fcard-800x500.png",{"path":851,"title":852,"description":853,"published":854,"category":663,"image":855,"draft":558},"\u002Fblog\u002Fsupabase-backup-github-action","Backup gratis de Supabase con una GitHub Action, y la trampa","Un backup de Supabase con GitHub Action es gratis y sirve a muchas apps. El workflow, el connection string correcto y el egress de cada ejecución.","2026-09-27","\u002Fblog\u002Fsupabase-backup-github-action\u002Fcard-800x500.png",{"path":857,"title":858,"description":859,"published":860,"category":663,"image":861,"draft":558},"\u002Fblog\u002Fdownload-your-supabase-backup","Por qué no puedes descargar tu backup de Supabase","No puedes descargar tu backup de Supabase en un proyecto actual: la copia diaria es una instantánea física. Cómo comprobarlo y tener tu propia copia.","2026-09-26","\u002Fblog\u002Fdownload-your-supabase-backup\u002Fcard-800x500.png",{"path":863,"title":864,"description":865,"published":860,"category":554,"image":866,"draft":558},"\u002Fblog\u002Fsupabase-permission-denied-for-table","Supabase \"permission denied for table\": el grant que falta","Desde el 30 de octubre, una tabla nueva de Supabase responde \"permission denied for table\" hasta que le das acceso. El grant del email es la mitad del arreglo.","\u002Fblog\u002Fsupabase-permission-denied-for-table\u002Fcard-800x500.png",{"path":868,"title":869,"description":870,"published":871,"category":554,"image":872,"draft":558},"\u002Fblog\u002Fopen-api-endpoint-exposed","¿Un endpoint de API abierto es un problema? Mira el JSON","Tu escaneo señaló un endpoint de API abierto. Que importe depende de lo que devolvió, y la mayoría de los que encontramos eran de la plataforma.","2026-09-25","\u002Fblog\u002Fopen-api-endpoint-exposed\u002Fcard-800x500.png",{"path":874,"title":875,"description":876,"published":877,"category":554,"image":878,"draft":558},"\u002Fblog\u002Flovable-security-scan","Escaneo de seguridad de Lovable: lo que no puede probar","Escaneo de seguridad de Lovable: qué revisan el Quick scan y el Deep scan, cuándo corre cada uno, y lo único que ningún escaneo desde dentro puede probar.","2026-09-24","\u002Fblog\u002Flovable-security-scan\u002Fcard-800x500.png",{"path":880,"title":881,"description":882,"published":883,"category":663,"image":884,"draft":558},"\u002Fblog\u002Fsupabase-backup-auth-users","Por qué tu dump de Supabase no tiene usuarios dentro","Ejecuta supabase db dump por sí solo y obtienes la forma de tu base de datos y ninguna de sus filas, sin el esquema auth donde viven tus usuarios.","2026-09-23","\u002Fblog\u002Fsupabase-backup-auth-users\u002Fcard-800x500.png",{"path":886,"title":887,"description":888,"published":889,"category":554,"image":890,"draft":558},"\u002Fblog\u002Fdomain-and-certificate-expiry","Dominio caducado, web caída: qué pasa a partir de ahora","Tu dominio caducó y tu web está caída. Aquí tienes el reloj que acaba de arrancar, por qué el certificado es el menor problema, y cómo comprobar ambos.","2026-09-22","\u002Fblog\u002Fdomain-and-certificate-expiry\u002Fcard-800x500.png",{"path":892,"title":893,"description":894,"published":895,"category":554,"image":896,"draft":558},"\u002Fblog\u002Fis-lovable-safe","¿Es seguro Lovable? Lo que vimos en 18.554 aplicaciones Lovable","¿Es seguro Lovable? Pasamos 18.554 aplicaciones Lovable por nueve comprobaciones. La plataforma fue la más limpia de cinco. Los hallazgos estaban dentro.","2026-09-21","\u002Fblog\u002Fis-lovable-safe\u002Fcard-800x500.png",{"path":898,"title":899,"description":900,"published":901,"category":554,"image":902,"draft":558},"\u002Fblog\u002Fvibe-coded-app-security-checklist","La lista de seguridad del vibe coding, en nueve comprobaciones","Una lista de seguridad del vibe coding con nueve puntos, cada uno verificable en tu app desde fuera por cualquiera, y cada uno con una prueba de una línea.","2026-09-20","\u002Fblog\u002Fvibe-coded-app-security-checklist\u002Fcard-800x500.png",{"path":423,"title":904,"description":905,"published":906,"category":554,"image":907,"draft":558},"Una clave secreta de Stripe en tu frontend puede mover dinero","Una clave secreta de Stripe expuesta en tu frontend puede reembolsar, cobrar y leer cada ficha de cliente que tienes. Tu clave pk_live_ sí debe estar ahí.","2026-09-19","\u002Fblog\u002Fstripe-secret-key-in-frontend\u002Fcard-800x500.png",{"path":909,"title":910,"description":911,"published":912,"category":554,"image":913,"draft":558},"\u002Fblog\u002Fvite-and-next-public-env-vars","Variables de entorno de Vite expuestas: el prefijo dice publica esto","Variables de entorno de Vite expuestas en tu app: hicieron lo que pedía el prefijo. VITE_ y NEXT_PUBLIC_ significan publica esto, y la IA nunca supo el coste.","2026-09-18","\u002Fblog\u002Fvite-and-next-public-env-vars\u002Fcard-800x500.png",{"path":915,"title":916,"description":917,"published":918,"category":554,"image":919,"draft":558},"\u002Fblog\u002Fbase44-source-maps","Seguridad en Base44: qué marca un escaneo y qué te toca arreglar a ti","Seguridad en Base44 en 5.438 apps escaneadas: tres hallazgos en casi todas son de la plataforma, el source map es la insignia de Base44 y lo tuyo es poco.","2026-09-17","\u002Fblog\u002Fbase44-source-maps\u002Fcard-800x500.png",{"path":921,"title":922,"description":923,"published":924,"category":554,"image":925,"draft":558},"\u002Fblog\u002Fis-cursor-ai-safe","¿Es seguro Cursor AI? El editor, el código y la app que publicaste","¿Es seguro Cursor AI? Tres preguntas en una búsqueda: qué guarda Cursor, en qué falla el código que escribe y si la app que publicaste está abierta.","2026-09-16","\u002Fblog\u002Fis-cursor-ai-safe\u002Fcard-800x500.png",{"path":927,"title":928,"description":929,"published":930,"category":554,"image":931,"draft":558},"\u002Fblog\u002Fis-replit-safe","¿Es seguro Replit? Lo que vimos en 3.042 aplicaciones Replit en vivo","¿Es seguro Replit? Pasamos 3.042 aplicaciones Replit en vivo por nueve comprobaciones externas. Los hallazgos estaban en la aplicación publicada, no en el host.","2026-09-15","\u002Fblog\u002Fis-replit-safe\u002Fcard-800x500.png",{"path":933,"title":934,"description":935,"published":936,"category":663,"image":937,"draft":558},"\u002Fblog\u002Fsupabase-storage-backup","Supabase storage backup: tu copia de la base no tiene archivos","Un Supabase storage backup es una tarea aparte. Las copias de la base guardan la lista de tus archivos y ninguno de ellos, así que cada upload queda roto.","2026-09-14","\u002Fblog\u002Fsupabase-storage-backup\u002Fcard-800x500.png",{"path":939,"title":940,"description":941,"published":942,"category":663,"image":943,"draft":558},"\u002Fblog\u002Fsupabase-point-in-time-recovery","Point-in-time recovery de Supabase: precio y límites","El point-in-time recovery de Supabase rebobina tu base de datos a cualquier segundo de la última semana. Cuesta $100 al mes sobre Pro y solo cubre la base.","2026-09-13","\u002Fblog\u002Fsupabase-point-in-time-recovery\u002Fcard-800x500.png",{"path":945,"title":946,"description":947,"published":948,"category":663,"image":949,"draft":558},"\u002Fblog\u002Fsupabase-project-paused-recover","¿Proyecto de Supabase pausado? Tus datos siguen ahí","Supabase pausó tu proyecto tras una semana sin actividad. No se ha borrado nada, Restaurar está junto al nombre y tienes un año antes de que eso cambie.","2026-09-12","\u002Fblog\u002Fsupabase-project-paused-recover\u002Fcard-800x500.png",{"path":951,"title":952,"description":953,"published":954,"category":554,"image":955,"draft":558},"\u002Fblog\u002Fsafest-ai-app-builder","¿Qué builder de apps con IA es más seguro? 30.998 apps escaneadas","¿Qué builder de apps con IA es más seguro? Escaneamos 30.998 apps activas de Lovable, Base44, Replit, v0 y Bolt. El builder no es lo que decide tu nota.","2026-09-11","\u002Fblog\u002Fsafest-ai-app-builder\u002Fcard-800x500.png",{"path":957,"title":958,"description":959,"published":960,"category":554,"image":961,"draft":558},"\u002Fblog\u002Fenable-rls-on-every-supabase-table","Activa Row Level Security en cada tabla de Supabase y compruébalo","Activar Row Level Security en Supabase sin política cierra la tabla del todo. Una política sin el interruptor no hace nada. Aquí tienes el SQL y la prueba.","2026-09-10","\u002Fblog\u002Fenable-rls-on-every-supabase-table\u002Fcard-800x500.png",{"path":963,"title":964,"description":965,"published":966,"category":554,"image":967,"draft":558},"\u002Fblog\u002Fsupabase-rls-disabled-in-public","Supabase \"RLS disabled in public\": lo que el aviso no ve","Supabase marca \"RLS disabled in public\" como error. No dice nada de la política de lectura que deja tu tabla igual de abierta a cualquiera.","2026-09-09","\u002Fblog\u002Fsupabase-rls-disabled-in-public\u002Fcard-800x500.png",{"path":969,"title":970,"description":971,"published":972,"category":554,"image":973,"draft":558},"\u002Fblog\u002Frotate-supabase-service-role-key","Cómo rotar una clave service_role de Supabase filtrada","Supabase dice que primero cierres la fuga. Otras guías dicen que rotes ya. Cuál es lo correcto depende de dónde se filtró tu clave service_role.","2026-09-08","\u002Fblog\u002Frotate-supabase-service-role-key\u002Fcard-800x500.png",{"path":975,"title":976,"description":977,"published":978,"category":554,"image":979,"draft":558},"\u002Fblog\u002Fvibe-coding-security-scanners-compared","Escáneres de seguridad vibe coding comparados, el nuestro incluido","El mejor escáner de seguridad para vibe coding depende de tres preguntas que ninguna lista de funciones responde. Ocho herramientas, con precios.","2026-09-07","\u002Fblog\u002Fvibe-coding-security-scanners-compared\u002Fcard-800x500.png",{"path":981,"title":982,"description":983,"published":984,"category":554,"image":985,"draft":558},"\u002Fblog\u002Fsupabase-security-checker","Supabase security checker: haz tú mismo las cinco comprobaciones","Un Supabase security checker lee tu app publicada en lugar de la configuración de tu proyecto. Estas son las cinco comprobaciones y cómo hacer cada una.","2026-09-06","\u002Fblog\u002Fsupabase-security-checker\u002Fcard-800x500.png",{"path":987,"title":988,"description":989,"published":990,"category":554,"image":991,"draft":558},"\u002Fblog\u002Fvibe-coding-security-scanner","Escáner de seguridad para vibe coding: qué se le escapa","Un escáner de seguridad para vibe coding lee tu app en vivo desde fuera. Qué cubre eso, las cuatro cosas que no puede ver y cómo leer el resultado.","2026-09-05","\u002Fblog\u002Fvibe-coding-security-scanner\u002Fcard-800x500.png",{"path":993,"title":994,"description":995,"published":996,"category":554,"image":997,"draft":558},"\u002Fblog\u002Fmissing-security-headers","Faltan cabeceras de seguridad: cuándo importa de verdad","Que falten cabeceras de seguridad es el hallazgo que más imprime nuestro escáner. Contra qué protegen y cuándo son la línea menos urgente de tu informe.","2026-09-04","\u002Fblog\u002Fmissing-security-headers\u002Fcard-800x500.png",{"path":999,"title":1000,"description":1001,"published":1002,"category":554,"image":1003,"draft":558},"\u002Fblog\u002Fopenai-api-key-exposed-in-frontend","Tu clave de API de OpenAI está expuesta en tu frontend. Rótala.","Una clave de API de OpenAI expuesta en tu frontend no se puede atar a un dominio. Rótala hoy, mueve la llamada a tu servidor y limita el gasto.","2026-09-03","\u002Fblog\u002Fopenai-api-key-exposed-in-frontend\u002Fcard-800x500.png",{"path":1005,"title":1006,"description":1007,"published":1008,"category":554,"image":1009,"draft":558},"\u002Fblog\u002Fwhat-secrets-leak-from-vibe-coded-apps","Clave de API expuesta en el frontend: qué llevan 30.998 apps","Una clave de API expuesta en tu frontend suele ser una clave de Google Maps. Escaneamos 30.998 apps vibe-coded y contamos qué secretos se filtran de verdad.","2026-09-02","\u002Fblog\u002Fwhat-secrets-leak-from-vibe-coded-apps\u002Fcard-800x500.png",{"path":1011,"title":1012,"description":1013,"published":1014,"category":663,"image":1015,"draft":558},"\u002Fblog\u002Fsupabase-backup-tools-compared","Herramientas de backup de Supabase comparadas, la nuestra incluida","Cuatro tipos de herramienta de backup de Supabase, qué copia cada una y cuándo una GitHub Action gratuita le gana a pagar a cualquiera, nosotros incluidos.","2026-09-01","\u002Fblog\u002Fsupabase-backup-tools-compared\u002Fcard-800x500.png",{"path":1017,"title":1018,"description":1019,"published":1020,"category":554,"image":1021,"draft":558},"\u002Fblog\u002Freplit-secrets-explained","Cómo usar secrets en Replit, y qué se publica de todos modos","Cómo usar secrets en Replit: añadir uno, leerlo y arreglar las dos causas del undefined. Y las claves que la herramienta Secrets no puede mantener privadas.","2026-08-31","\u002Fblog\u002Freplit-secrets-explained\u002Fcard-800x500.png",{"path":1023,"title":1024,"description":1025,"published":1026,"category":663,"image":1027,"draft":558},"\u002Fblog\u002Fback-up-supabase-free-tier","Backups del plan gratuito de Supabase: cómo hacer uno sin terminal","No hay backups en el plan gratuito de Supabase: la copia tienes que hacerla tú. Cómo sacarla desde el panel y qué deja fuera la vía CSV.","2026-08-30","\u002Fblog\u002Fback-up-supabase-free-tier\u002Fcard-800x500.png",{"path":1029,"title":1030,"description":1031,"published":1032,"category":554,"image":1033,"draft":558},"\u002Fblog\u002Fis-supabase-secure","¿Supabase es seguro? Sí. Tu proyecto es otra pregunta","¿Supabase es seguro? La plataforma está auditada, cifrada y sometida a pentests. Su propia documentación dice dónde termina eso y empiezan tus ajustes.","2026-08-29","\u002Fblog\u002Fis-supabase-secure\u002Fcard-800x500.png",{"path":1035,"title":1036,"description":1037,"published":1038,"category":554,"image":1039,"draft":558},"\u002Fblog\u002Fwhere-to-find-supabase-api-keys","Dónde están tus claves API de Supabase: anon, service_role y URL","La URL del proyecto, la clave anon y la clave service_role están en una sola página del panel. Aquí tienes dónde está, y cuál de los cuatro va en tu app.","2026-08-28","\u002Fblog\u002Fwhere-to-find-supabase-api-keys\u002Fcard-800x500.png",{"path":1041,"title":1042,"description":1043,"published":1044,"category":554,"image":1045,"draft":558},"\u002Fblog\u002Fnew-row-violates-row-level-security-policy","New row violates row-level security policy en Supabase. ¿Y ahora?","\"New row violates row-level security policy\" significa que Supabase rechazó una escritura. El arreglo rápido vuelve a abrir la tabla para cualquiera.","2026-08-27","\u002Fblog\u002Fnew-row-violates-row-level-security-policy\u002Fcard-800x500.png",{"path":1047,"title":1048,"description":1049,"published":1050,"category":554,"image":1051,"draft":558},"\u002Fblog\u002Fcors-wildcard-security-risk","¿Es un comodín CORS un riesgo de seguridad? Casi nunca.","¿Es un comodín CORS un riesgo de seguridad? Casi siempre es el ajuste por defecto de tu builder y no entrega nada que tu servidor no diera ya a cualquiera.","2026-08-26","\u002Fblog\u002Fcors-wildcard-security-risk\u002Fcard-800x500.png",{"path":1053,"title":1054,"description":1055,"published":1056,"category":663,"image":1057,"draft":558},"\u002Fblog\u002Fsupabase-branching-is-not-a-backup","Supabase branching no es un backup. Solo va hacia adelante.","Por qué el branching de Supabase no es un backup: una rama arranca sin tus datos y un merge solo mueve el esquema. Para qué sirve y qué usar en su lugar.","2026-08-25","\u002Fblog\u002Fsupabase-branching-is-not-a-backup\u002Fcard-800x500.png",{"path":1059,"title":1060,"description":1061,"published":1062,"category":663,"image":1063,"draft":558},"\u002Fblog\u002Fhow-to-restore-a-supabase-backup","Cómo restaurar un backup de Supabase, y qué se rompe después","Cómo restaurar un backup de Supabase desde el panel o desde un archivo de volcado, qué reemplaza la restauración y por qué tu app puede seguir rota al terminar.","2026-08-24","\u002Fblog\u002Fhow-to-restore-a-supabase-backup\u002Fcard-800x500.png",{"path":1065,"title":1066,"description":1067,"published":1068,"category":554,"image":1069,"draft":558},"\u002Fblog\u002Fsupabase-storage-bucket-public","Tu bucket de Supabase Storage es público. ¿Es un problema?","Un bucket público de Supabase Storage significa que quien tenga la URL de un archivo puede abrirlo. No significa que alguien pueda listar lo que hay dentro.","2026-08-23","\u002Fblog\u002Fsupabase-storage-bucket-public\u002Fcard-800x500.png",{"path":1071,"title":1072,"description":1073,"published":1074,"category":554,"image":1075,"draft":558},"\u002Fblog\u002Fgoogle-api-key-exposed-in-frontend","¿Es un problema una clave de API de Google en tu frontend?","Una clave de API de Google en tu frontend es la clave que más encuentra nuestro escáner, y casi siempre no pasa nada. Un ajuste gratuito decide cuál es cuál.","2026-08-22","\u002Fblog\u002Fgoogle-api-key-exposed-in-frontend\u002Fcard-800x500.png",{"path":1077,"title":1078,"description":1079,"published":1080,"category":663,"image":1081,"draft":558},"\u002Fblog\u002Fai-agent-deleted-my-database","Un agente de IA borró mis datos de Supabase. ¿Qué puedo recuperar?","Un agente de IA borró datos de tu base. Lo que puedes recuperar ya estaba decidido antes de que se ejecutara, y los próximos minutos deciden el resto.","2026-08-21","\u002Fblog\u002Fai-agent-deleted-my-database\u002Fcard-800x500.png",{"path":1083,"title":1084,"description":1085,"published":1086,"category":554,"image":1087,"draft":558},"\u002Fblog\u002Fcan-anyone-read-your-supabase-database","¿Puede cualquiera leer tu base de datos Supabase? 3.680 apps","¿Puede cualquiera leer tu base de datos Supabase sin iniciar sesión? Escaneamos 30.998 apps activas hechas con builders de IA y medimos cuántas dicen que sí.","2026-08-18","\u002Fblog\u002Fcan-anyone-read-your-supabase-database\u002Fcard-800x500.png",{"path":1089,"title":1090,"description":1091,"published":1086,"category":554,"image":1092,"draft":558},"\u002Fblog\u002Fsource-maps-exposed-in-production","Source maps expuestos: tu app está publicando su código original","Un source map expuesto deja que cualquiera lea el código original de tu app, comentarios incluidos. La comprobación de 30 segundos y lo que de verdad importa.","\u002Fblog\u002Fsource-maps-exposed-in-production\u002Fcard-800x500.png",{"path":1094,"title":1095,"description":1096,"published":1097,"category":554,"image":1098,"draft":558},"\u002Fblog\u002Fsupabase-new-api-keys","Supabase publishable key: ¿es la misma que la anon key?","Supabase sustituyó anon y service_role por claves publicables y secretas. ¿Cuál pertenece a tu app y cuál no debe estar nunca?","2026-08-12","\u002Fblog\u002Fsupabase-new-api-keys\u002Fcard-800x500.png",{"path":1100,"title":1101,"description":1102,"published":1103,"category":663,"image":1104,"draft":558},"\u002Fblog\u002Fdoes-supabase-back-up-my-database","¿Supabase respalda mi base de datos? Depende de tu plan.","¿Supabase respalda tu base de datos? A diario en los planes de pago, y nada en el gratuito. Cómo saber cuál tienes y qué es lo que esa copia no cubre.","2026-08-11","\u002Fblog\u002Fdoes-supabase-back-up-my-database\u002Fcard-800x500.png",{"path":1106,"title":1107,"description":1108,"published":1109,"category":554,"image":1110,"draft":558},"\u002Fblog\u002Fsupabase-rls-on-but-table-still-public","Supabase Row Level Security está activado. Tu tabla sigue pública.","Activar Supabase Row Level Security no protege una tabla. Lo hacen tus políticas, y la que arregló tu app rota puede dejar entrar a cualquiera.","2026-08-10","\u002Fblog\u002Fsupabase-rls-on-but-table-still-public\u002Fcard-800x500.png",{"path":1112,"title":1113,"description":1114,"published":1109,"category":663,"image":1115,"draft":558},"\u002Fblog\u002Fversion-history-is-not-a-backup","El historial de versiones no es un backup. No recupera una tabla.","Lovable y Bolt guardan un historial de versiones de tu código. Tu base de datos es un servicio aparte, y volver atrás no te devuelve tus datos.","\u002Fblog\u002Fversion-history-is-not-a-backup\u002Fcard-800x500.png",{"path":1117,"title":1118,"description":1119,"published":1120,"category":663,"image":1121,"draft":558},"\u002Fblog\u002Fthree-ways-to-back-up-a-supabase-database","Tres formas de respaldar una base Supabase, y qué falta en cada una","El panel, pg_dump y un servicio gestionado. Qué guarda de verdad cada uno, qué deja fuera sin avisar y cuál sobrevive a perder la cuenta.","2026-08-09","\u002Fblog\u002Fthree-ways-to-back-up-a-supabase-database\u002Fcard-800x500.png",{"path":1123,"title":1124,"description":1125,"published":1120,"category":554,"image":1126,"draft":558},"\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend","Qué claves de API son seguras en el navegador y cuáles no","Tu clave anon de Supabase está pensada para ser pública. Tu clave service_role no, y se salta todas las reglas que pongas. Cómo distinguirlas.","\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend\u002Fcard-800x500.png",1794213644971]