[{"data":1,"prerenderedAt":1200},["ShallowReactive",2],{"blog-es-email-link-is-invalid-or-has-expired":3,"blog-index-es":673},{"id":4,"title":5,"body":6,"category":628,"cover":629,"coverAlt":630,"description":631,"draft":632,"extension":633,"faq":634,"image":653,"keywords":654,"meta":661,"navigation":662,"ogTitle":663,"path":664,"published":665,"seo":666,"stem":667,"tldr":668,"updated":665,"__hash__":672},"blog_es\u002Fblog\u002Femail-link-is-invalid-or-has-expired.md","Supabase \"Email link is invalid or has expired\" con un enlace nuevo",{"type":7,"value":8,"toc":615},"minimark",[9,13,24,27,30,38,43,46,61,85,92,96,99,114,117,123,126,142,146,149,161,178,190,194,197,206,209,215,222,226,233,240,246,257,321,329,333,336,342,348,381,387,393,404,408,411,421,427,430,436,442,455,458,464,471,490,495,498,504,510,514,517,526,540,549,569,573,604],[10,11,12],"p",{},"Un usuario nuevo se registra, abre el email de confirmación un minuto después y\npulsa el botón. Tu app se abre en la página de inicio, todavía sin sesión, y la\nbarra de direcciones termina así:",[14,15,20],"pre",{"className":16,"code":18,"language":19},[17],"language-text","#error=access_denied&error_code=otp_expired&error_description=Email+link+is+invalid+or+has+expired\n","text",[21,22,18],"code",{"__ignoreMap":23},"",[10,25,26],{},"Te registras con tu propia dirección para comprobarlo. El enlace funciona a la\nprimera, y en cada intento después.",[10,28,29],{},"Supabase dice \"Email link is invalid or has expired\" cuando el token de un\nenlace de email ya se usó, fue sustituido o pasó de su hora. En un enlace que\nllegó hace un minuto, el motivo habitual es que algo lo usó antes que tu\nusuario: un filtro de correo que abre los enlaces para revisarlos. Tu app de\nLovable, Bolt, v0, Cursor, Claude Code o Replit funciona tal como se construyó,\ny el email que envía se puede cambiar para que un filtro no tenga nada que\ngastar.",[10,31,32,33,37],{},"Esta es la parte que el consejo habitual hace mal: ",[34,35,36],"strong",{},"\"pídele que lo intente otra\nvez\" y \"haz que el enlace dure más\" fallan los dos ante un filtro."," Un segundo\nenlace pasa por el mismo filtro y acaba igual, por mucho que fuera a durar.",[39,40,42],"h2",{"id":41},"qué-significa-email-link-is-invalid-or-has-expired-en-supabase","¿Qué significa \"Email link is invalid or has expired\" en Supabase?",[10,44,45],{},"Significa que Supabase Auth, la parte de Supabase que inicia la sesión de la\ngente, no encontró detrás del enlace un token que estuviera dispuesto a aceptar.",[10,47,48,49,56,57,60],{},"El mensaje está escrito en el propio código del servidor de Auth. En\n",[50,51,55],"a",{"href":52,"rel":53},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fauth\u002Fblob\u002Fmaster\u002Finternal\u002Fapi\u002Fverify.go",[54],"nofollow","la función que comprueba un enlace de email",",\nse devuelve en dos puntos: cuando ningún token pendiente coincide con el del\nenlace, y cuando el token que coincide se envió hace más tiempo del que tu\nproyecto permite. Las dos respuestas son HTTP 403 con el código ",[21,58,59],{},"otp_expired",",\nasí que el mensaje por sí solo no te dice cuál de las dos ocurrió.",[10,62,63,64,67,68,71,72,75,76,79,80,84],{},"Tu usuario nunca ve el 403. El enlace del email apunta a la dirección propia de\ntu proyecto de Supabase, que comprueba el token y luego envía el navegador a tu\napp. Cuando la comprobación falla, Supabase pone tres cosas en la parte de la\ndirección de tu app que va detrás del ",[21,65,66],{},"#",": ",[21,69,70],{},"error=access_denied",",\n",[21,73,74],{},"error_code=otp_expired"," y el propio mensaje. Si tu app se configuró con el\nflujo PKCE, también están en la parte que va detrás del ",[21,77,78],{},"?",". Mientras ninguna\npágina de tu app los lea, la persona ve lo que tu app le muestra a un visitante\nsin sesión, sin una palabra sobre el porqué. Una página para restablecer la\ncontraseña que muestra su formulario de todos modos falla al guardar con \"Auth\nsession missing!\", porque el enlace no inició la sesión de nadie, y\n",[50,81,83],{"href":82},"\u002Fblog\u002Fauth-session-missing","ese mensaje tiene su propio artículo",".",[10,86,87,88,84],{},"Piensa en el enlace como una entrada que deja pasar a una persona una sola vez.\nQuien la entrega primero pasa, y la entrada queda perforada. \"Email link is\ninvalid or has expired\" es la persona en la puerta con una entrada que ya tiene\nun agujero, o con una cuya hora ya pasó. Si tus usuarios entran y más tarde la app\nlos echa, es otro error con\n",[50,89,91],{"href":90},"\u002Fblog\u002Fsupabase-users-logged-out","una página propia",[39,93,95],{"id":94},"por-qué-un-enlace-enviado-hace-un-minuto-ya-está-usado","Por qué un enlace enviado hace un minuto ya está usado",[10,97,98],{},"Porque algo lo abrió antes que tu usuario, y abrir un enlace de email de\nSupabase es lo mismo que usarlo.",[10,100,101,102,107,108,113],{},"La\n",[50,103,106],{"href":104,"rel":105},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fauth-email-templates",[54],"guía de plantillas de email","\nde Supabase lo incluye entre sus limitaciones: algunos proveedores de correo\ntienen detección de spam u otras funciones de seguridad que abren por adelantado\nlos enlaces de los emails entrantes. El ejemplo que da es Safe Links de\nMicrosoft Defender for Office 365. La\n",[50,109,112],{"href":110,"rel":111},"https:\u002F\u002Fsupabase.com\u002Fkb\u002Ftroubleshooting\u002Fotp-verification-failures-token-has-expired-or-otp_expired-errors",[54],"página de Supabase para solucionar este error","\ndice que esa apertura por adelantado es el motivo más común de que un token\nparezca caducado o no válido antes de que el usuario pueda siquiera usarlo.",[10,115,116],{},"El enlace por defecto no tiene ningún paso entre abrirlo y usarlo. Su dirección\nes el endpoint de verificación de tu proyecto, que comprueba el token y lo gasta\nen la primera visita, la haga quien la haga. Así que el filtro entrega la\nentrada mientras llega el mensaje, y pasa. Tu usuario hace clic un minuto\ndespués y lo rechazan en la puerta.",[118,119],"diagram",{"alt":120,"caption":121,"src":122},"Un email con una entrada dentro. De él salen dos caminos hacia la misma puerta. El primero, desde una lupa que representa el filtro de correo, llega enseguida a la puerta y recibe un visto. El segundo, desde la persona, llega después y recibe una cruz roja, porque la entrada ya tiene un agujero perforado.","La primera visita usa el enlace. Un filtro de correo que lo abre para revisarlo llega antes que la persona.","\u002Fblog\u002Femail-link-is-invalid-or-has-expired\u002Fwho-used-it-first-1600x720.png",[10,124,125],{},"De ahí salen dos cosas.",[127,128,129,136],"ul",{},[130,131,132,135],"li",{},[34,133,134],{},"Tú no lo vas a ver."," Le pasa a la gente cuyo correo pasa por un filtro de\nese tipo, y puede que tu propia bandeja nunca sea una de ellas. Tus registros\nde prueba siguen funcionando mientras tus usuarios te escriben.",[130,137,138,141],{},[34,139,140],{},"La cuenta suele estar ya confirmada."," En una confirmación de registro, la\nvisita del filtro cuenta: Supabase confirma la dirección de quien abra el\nenlace. Si lo abrió un filtro, la cuenta está confirmada cuando tu usuario ve\nel error, y puede iniciar sesión con la contraseña que eligió.",[39,143,145],{"id":144},"las-otras-tres-causas","Las otras tres causas",[10,147,148],{},"Cada una produce el mismo mensaje, y cada una te pide algo distinto.",[10,150,151,154,155,160],{},[34,152,153],{},"El enlace ya se usó."," Tu usuario lo pulsó una vez, quizá en el móvil, y\nahora pulsa el mismo email en el portátil. Cuando un enlace funciona, Supabase\nborra todos los enlaces que seguían pendientes para esa cuenta. Su\n",[50,156,159],{"href":157,"rel":158},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fauth\u002Fblob\u002Fmaster\u002Finternal\u002Fmodels\u002Fuser.go",[54],"código de cuentas","\nhace la misma limpieza después de una confirmación y después de un enlace de\ninicio de sesión, así que un segundo clic no encuentra nada. La entrada quedó\nperforada en la primera visita.",[10,162,163,166,167,172,173,177],{},[34,164,165],{},"Un email más reciente lo sustituyó."," Pulsar \"reenviar\" crea un enlace nuevo,\ny Supabase borra el anterior del mismo tipo antes de guardarlo. Ocurre en\n",[50,168,171],{"href":169,"rel":170},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fauth\u002Fblob\u002Fmaster\u002Finternal\u002Fmodels\u002Fone_time_token.go",[54],"el código que crea el token",".\nEl primer email sigue en la bandeja con buen aspecto, y su enlace falla. Solo\nfunciona el email más reciente. Supabase además solo deja que una dirección pida\neste tipo de email una vez cada 60 segundos, algo que\n",[50,174,176],{"href":175},"\u002Fblog\u002Fsupabase-email-rate-limit-exceeded","el límite de emails"," explica a fondo.",[10,179,180,183,184,189],{},[34,181,182],{},"De verdad caducó."," La\n",[50,185,188],{"href":186,"rel":187},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fauth-email-passwordless",[54],"guía de inicio de sesión sin contraseña","\nde Supabase dice que los magic links caducan por defecto al cabo de 1 hora. La\nmisma página dice que el ajuste Email OTP Expiration rige también la validez de\nlos magic links y de los demás enlaces de email, incluidos los de confirmación,\nrecuperación de contraseña, cambio de email e invitación. Quien se registra por\nla noche y abre el email a la mañana siguiente se encuentra este mensaje, haga\nlo que haga su servicio de correo.",[39,191,193],{"id":192},"qué-pasa-cuando-el-enlace-se-abre-en-otro-dispositivo","¿Qué pasa cuando el enlace se abre en otro dispositivo?",[10,195,196],{},"Si tu app usa el flujo PKCE, el otro dispositivo falla primero con un mensaje\ndistinto, y este viene después.",[10,198,199,200,205],{},"PKCE parte la entrada en dos. La\n",[50,201,204],{"href":202,"rel":203},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fsessions\u002Fpkce-flow",[54],"guía de PKCE"," de\nSupabase explica que el code verifier se crea y se guarda en local cuando\nempieza el flujo de Auth, y que por eso el intercambio del código tiene que\niniciarse en el mismo navegador y el mismo dispositivo donde empezó el flujo. El\nemail lleva una mitad. El navegador donde tu usuario pidió el enlace se queda\ncon el talón.",[10,207,208],{},"Abierto en un móvil, el enlace pasa igualmente por la comprobación de Supabase,\nque lo gasta. Luego tu app busca el talón en el móvil y no lo encuentra. El\ncliente de JavaScript de Supabase lo informa con sus propias palabras:",[14,210,213],{"className":211,"code":212,"language":19},[17],"PKCE code verifier not found in storage. This can happen if the auth flow was initiated in a different browser or device, or if the storage was cleared.\n",[21,214,212],{"__ignoreMap":23},[10,216,217,218,221],{},"Así que la persona vuelve al portátil y pulsa allí el mismo email, y Supabase\nresponde \"Email link is invalid or has expired\", porque el móvil ya hizo\nperforar la entrada. Si la dirección en la que se abre tu app tras un enlace que\nfunciona contiene ",[21,219,220],{},"?code=",", tu app usa PKCE y este caso te afecta.",[39,223,225],{"id":224},"cómo-sé-cuál-de-ellas-fue","¿Cómo sé cuál de ellas fue?",[10,227,228,229,232],{},"Pregúntale a tu propia base de datos. Cada cuenta de tu proyecto es una fila en\nuna tabla llamada ",[21,230,231],{},"auth.users",", y esa fila registra cuándo se envió cada tipo\nde email y cuándo se confirmó la dirección.",[10,234,235,236,239],{},"Abre el ",[34,237,238],{},"SQL Editor"," en tu panel de Supabase y ejecuta esto con la dirección\nque te dio la persona:",[14,241,244],{"className":242,"code":243,"language":19},[17],"select email,\n       confirmation_sent_at,\n       email_confirmed_at,\n       recovery_sent_at\nfrom auth.users\nwhere lower(email) = lower('person@example.com');\n",[21,245,243],{"__ignoreMap":23},[10,247,248,249,252,253,256],{},"Lee una fila y no cambia nada. ",[21,250,251],{},"confirmation_sent_at"," es cuándo salió la última\nconfirmación de registro. ",[21,254,255],{},"recovery_sent_at"," es cuándo salió el último magic\nlink o restablecimiento de contraseña hacia una cuenta que ya existía. Son los\nmomentos desde los que Supabase mide la hora, en la misma función que devuelve\nel error. Compáralos con la hora del email que pulsó la persona.",[258,259,260,273],"table",{},[261,262,263],"thead",{},[264,265,266,270],"tr",{},[267,268,269],"th",{},"Lo que ves",[267,271,272],{},"Lo que significa",[274,275,276,289,300,313],"tbody",{},[264,277,278,286],{},[279,280,281,282,285],"td",{},"Un enlace de confirmación falló en el primer clic, y ",[21,283,284],{},"email_confirmed_at"," tiene valor",[279,287,288],{},"Algo abrió el enlace antes que la persona. La cuenta está confirmada, así que puede entrar con su contraseña",[264,290,291,297],{},[279,292,293,294,296],{},"Un enlace de confirmación falló, ",[21,295,284],{}," está vacío y lo pulsó más de una hora después del envío",[279,298,299],{},"Caducó. Envía una confirmación nueva",[264,301,302,310],{},[279,303,304,306,307,309],{},[21,305,251],{}," o ",[21,308,255],{}," es posterior al email que pulsó",[279,311,312],{},"Un email más reciente lo sustituyó. Solo funciona el enlace del más nuevo",[264,314,315,318],{},[279,316,317],{},"Un magic link del email más reciente falló en el primer clic, dentro de la hora",[279,319,320],{},"Algo lo abrió primero: un filtro de correo, o la persona en otro dispositivo",[10,322,323,324,328],{},"Si la fila muestra la dirección confirmada y la persona sigue sin poder entrar\ncon contraseña, ",[50,325,327],{"href":326},"\u002Fblog\u002Fsupabase-invalid-login","Invalid login credentials"," es el\nsiguiente mensaje sobre el que leer.",[39,330,332],{"id":331},"qué-no-cambiar-mientras-lo-arreglas","Qué no cambiar mientras lo arreglas",[10,334,335],{},"La validez, el interruptor Confirm email y quién puede confirmar a un usuario.\nCada uno de los atajos obvios hace que el mensaje pare y abre algo.",[10,337,338,341],{},[34,339,340],{},"Deja la validez en una hora."," Un filtro abre el enlace mientras llega el\nmensaje, así que un enlace que dura una semana también lo usa él primero. La\nguía de inicio de sesión sin contraseña de Supabase desaconseja expresamente\nmás de 86.400 segundos, es decir, un día. Un enlace de inicio de sesión abre la\nsesión de quien lo abra. Cuanto más tiempo funciona, más tiempo son un email\nreenviado, una bandeja compartida o un mensaje viejo en el archivo de alguien\nuna forma de entrar en esa cuenta.",[10,343,344,347],{},[34,345,346],{},"Deja Confirm email activado."," Si lo desactivas, los registros dejan de\nenviar ningún enlace y este error se acaba. También significa que cualquiera\npuede crear una cuenta con una dirección que nunca ha controlado.",[10,349,350,353,354,357,358,363,364,367,368,371,372,376,377,84],{},[34,351,352],{},"No confirmes usuarios desde el navegador."," Pídele a un asistente que \"los\nmarque como confirmados y ya\" y puede echar mano de las funciones de\nadministración de Supabase, que pueden poner ",[21,355,356],{},"email_confirm"," en una cuenta. La\n",[50,359,362],{"href":360,"rel":361},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Freference\u002Fjavascript\u002Fadmin-api",[54],"referencia de administración","\nde Supabase dice que cualquier método bajo ",[21,365,366],{},"supabase.auth.admin"," requiere una\nclave ",[21,369,370],{},"secret"," y que esa clave nunca debe exponerse en el navegador. Una clave\nsecreta en tu frontend lee y escribe todas las tablas de tu base de datos,\ndigan lo que digan tus reglas, para cualquiera que abra el código de tu sitio.\nEs la clave que\n",[50,373,375],{"href":374},"\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend","este artículo sobre claves de API","\nte dice que rotes. Si no estás seguro de que no se coló ninguna, nuestro\nescaneo gratuito lee el código que tu app en vivo envía a cada visitante y\nmarca las claves secretas que encuentra ahí. Tarda unos 20 segundos y no pide\ncuenta: ",[50,378,380],{"href":379},"\u002Fsecurity-scanner","escanea tu app",[10,382,383,386],{},[34,384,385],{},"Comprueba cualquier dirección que pase tu página de confirmación."," La segunda\nsolución que propone Supabase es una página tuya con un botón, a la que se\nllega con un enlace de esta forma:",[14,388,391],{"className":389,"code":390,"language":19},[17],"\u003Ca href=\"{{ .SiteURL }}\u002Fconfirm-signup?confirmation_url={{ .ConfirmationURL }}\">\n",[21,392,390],{"__ignoreMap":23},[10,394,395,396,399,400,403],{},"El botón lleva entonces adonde diga ",[21,397,398],{},"confirmation_url",". Hecha sin comprobación,\nesa página envía a cualquiera que siga un enlace manipulado a cualquier\ndirección, con el nombre de tu app. O compruebas que la dirección empieza por la\nde tu propio proyecto, ",[21,401,402],{},"https:\u002F\u002F\u003Cproject-ref>.supabase.co\u002Fauth\u002Fv1\u002Fverify",", o\nusas la versión con el token de la siguiente sección, que no hace pasar ninguna\ndirección por la página.",[39,405,407],{"id":406},"cómo-enviar-un-enlace-que-un-filtro-de-correo-no-pueda-gastar","Cómo enviar un enlace que un filtro de correo no pueda gastar",[10,409,410],{},"Haz que abrir la dirección no haga nada, de modo que solo inicie sesión una\npersona que escribe un código o pulsa un botón. Supabase documenta las dos\nopciones, y se cambian en las plantillas de email, dentro de Authentication en\ntu panel. Confirm signup y Magic Link son las dos que hay que editar.",[10,412,413,416,417,420],{},[34,414,415],{},"Envía un código."," Pon ",[21,418,419],{},"{{ .Token }}"," en la plantilla. La guía de Supabase lo\ndescribe como una contraseña de un solo uso de 6 dígitos que puede usarse en\nlugar del enlace. Tu página pide la dirección de email y el código, y luego\nllama a:",[14,422,425],{"className":423,"code":424,"language":19},[17],"const { error } = await supabase.auth.verifyOtp({ email, token, type: 'email' })\n",[21,426,424],{"__ignoreMap":23},[10,428,429],{},"Un filtro puede abrir un email. No puede escribir un código en tu página.",[10,431,432,435],{},[34,433,434],{},"Enlaza a una página con un botón."," Haz que el enlace apunte a una página\ntuya y lleve el hash del token, para que no tenga que pasar ninguna dirección:",[14,437,440],{"className":438,"code":439,"language":19},[17],"\u003Ca href=\"{{ .SiteURL }}\u002Fconfirm?token_hash={{ .TokenHash }}&type=email\">Confirm your email\u003C\u002Fa>\n",[21,441,439],{"__ignoreMap":23},[10,443,444,447,448,71,451,84],{},[21,445,446],{},"{{ .SiteURL }}"," es la Site URL de la página URL Configuration de tu proyecto.\nSi todavía dice ",[21,449,450],{},"http:\u002F\u002Flocalhost:3000",[50,452,454],{"href":453},"\u002Fblog\u002Fsupabase-auth-redirects-to-localhost","corrige eso primero",[10,456,457],{},"La página muestra un botón, y solo el botón hace el trabajo:",[14,459,462],{"className":460,"code":461,"language":19},[17],"const params = new URLSearchParams(window.location.search)\n\nasync function onConfirmClick() {\n  const { error } = await supabase.auth.verifyOtp({\n    token_hash: params.get('token_hash'),\n    type: 'email',\n  })\n  if (error?.code === 'otp_expired') showSendNewLink()\n}\n",[21,463,461],{"__ignoreMap":23},[10,465,466,467,470],{},"El tipo ",[21,468,469],{},"email"," sirve para las dos plantillas: la comprobación de Supabase busca\nel hash tanto entre las confirmaciones de registro como entre los enlaces de\ninicio de sesión.",[10,472,473,183,480,485,486,489],{},[34,474,475,476,479],{},"La página no debe llamar a ",[21,477,478],{},"verifyOtp"," al cargarse.",[50,481,484],{"href":482,"rel":483},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fpasswords",[54],"guía de contraseñas"," de\nSupabase construye su enlace de confirmación como\n",[21,487,488],{},"{{ .SiteURL }}\u002Fauth\u002Fconfirm?token_hash={{ .TokenHash }}&type=email&next={{ .RedirectTo }}","\ny lo verifica en el momento en que se abre esa dirección. Eso es otra vez el\nenlace por defecto en una dirección nueva, y un filtro lo abre con la misma\nfacilidad.",[118,491],{"alt":492,"caption":493,"src":494},"Dos emails uno al lado del otro. A la izquierda, el enlace va directo a la puerta: la lupa lo abre primero y recibe un visto, y la persona que va detrás, una cruz roja. A la derecha, el enlace abre una página con un botón: la lupa abre la página y no pasa nada, la entrada sigue entera, y la persona pulsa el botón y recibe el visto.","Con el enlace por defecto, abrirlo ya inicia la sesión. Con un botón o un código escrito, solo lo hace la acción de la persona.","\u002Fblog\u002Femail-link-is-invalid-or-has-expired\u002Fopening-is-not-using-1600x760.png",[10,496,497],{},"Cambies lo que cambies, habrá gente que siga encontrándose un enlace viejo, así\nque dale una salida. La página a la que llegan tus enlaces puede leer el error y\nofrecer un email nuevo:",[14,499,502],{"className":500,"code":501,"language":19},[17],"const hash = new URLSearchParams(window.location.hash.slice(1))\nif (hash.get('error_code') === 'otp_expired') showSendNewLink()\n",[21,503,501],{"__ignoreMap":23},[10,505,506,507,509],{},"En un registro, añade una línea que diga que quizá ya está confirmada y puede\nprobar a iniciar sesión. Si un builder escribe tus páginas de inicio de sesión,\npídele la versión con código o con botón de las dos plantillas, una página de\nllegada que lea ",[21,508,59],{}," y ofrezca un enlace nuevo, y ninguna clave de\nadministración en ninguna parte de la app.",[39,511,513],{"id":512},"enterarte-de-los-problemas-antes-de-que-tus-usuarios-te-escriban","Enterarte de los problemas antes de que tus usuarios te escriban",[10,515,516],{},"Este error les llega a tus usuarios y nunca a ti, y otros fallos también les\nllegan a ellos primero: la app que se cae de noche, o un prompt posterior que\nmete una clave en el código de tu app.",[10,518,519],{},[34,520,521,525],{},[50,522,524],{"href":523},"\u002Fwebsite-monitoring","Reeve Monitor"," sigue comprobando tu app en vivo desde fuera:",[127,527,528,531,534,537],{},[130,529,530],{},"si está disponible, cada 60 segundos, para distinguir \"el enlace no funciona\" de \"el sitio está caído\"",[130,532,533],{},"las nueve comprobaciones cada hora, en hasta tres apps, incluidas las claves secretas en el código que tu app envía a los visitantes",[130,535,536],{},"un mensaje cuando un resultado cambia, para que una clave que sale con un despliegue posterior te llegue sin que tengas que ir a mirar",[130,538,539],{},"un informe mensual, en lenguaje claro, de lo que vio",[10,541,542],{},[34,543,544,548],{},[50,545,547],{"href":546},"\u002Fsupabase-backups","Reeve Care"," guarda una copia de tu base de datos de\nSupabase, con las cuentas incluidas:",[127,550,551,554,557,560,563,566],{},[130,552,553],{},"una copia cifrada según una programación, en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase",[130,555,556],{},"el esquema auth en el que viven las cuentas de tus usuarios, para que vuelvan con todo lo demás",[130,558,559],{},"cada copia verificada antes de contar, contando las filas de cada tabla",[130,561,562],{},"también tus archivos subidos, en cuanto conectes una credencial de Storage",[130,564,565],{},"una restauración en un clic, con una copia de seguridad del estado actual tomada antes",[130,567,568],{},"todo lo que hace Monitor",[39,570,572],{"id":571},"qué-hacer-ahora","Qué hacer ahora",[574,575,576],"key-takeaways",{},[127,577,578,587,590,595,601],{},[130,579,580,581,583,584,586],{},"Lee la fila de la persona en ",[21,582,231],{},". Un ",[21,585,284],{}," con valor en una confirmación fallida significa que un filtro abrió el enlace y que puede iniciar sesión.",[130,588,589],{},"Di a la gente que use el email más reciente y, si tu app usa PKCE, que lo abra en el dispositivo donde lo pidió.",[130,591,592,593,84],{},"Cambia las plantillas Confirm signup y Magic Link para que envíen un código, o para que abran una página donde un botón llame a ",[21,594,478],{},[130,596,597,598,600],{},"Haz que la página a la que llegan tus enlaces lea ",[21,599,74],{}," y ofrezca un enlace nuevo.",[130,602,603],{},"Deja la validez en una hora, deja Confirm email activado y mantén la clave secreta fuera del navegador.",[10,605,606,607,609,610,614],{},"Si los emails ni siquiera llegan, ",[50,608,176],{"href":175},"\nes lo primero que hay que descartar. La\n",[50,611,613],{"href":612},"\u002Fchecklist","lista de seguridad de 10 minutos"," cubre lo que suele quedar abierto\nen una app recién lanzada.",{"title":23,"searchDepth":616,"depth":616,"links":617},3,[618,620,621,622,623,624,625,626,627],{"id":41,"depth":619,"text":42},2,{"id":94,"depth":619,"text":95},{"id":144,"depth":619,"text":145},{"id":192,"depth":619,"text":193},{"id":224,"depth":619,"text":225},{"id":331,"depth":619,"text":332},{"id":406,"depth":619,"text":407},{"id":512,"depth":619,"text":513},{"id":571,"depth":619,"text":572},"Fundamentos de seguridad","\u002Fblog\u002Femail-link-is-invalid-or-has-expired\u002Fcover-1200x630.png","Un sobre abierto con una entrada asomando a medias, y la entrada ya tiene un agujero perforado.","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.",false,"md",[635,638,641,644,647,650],{"q":636,"a":637},"¿Por qué un magic link de Supabase dice que ha caducado justo después de enviarlo?","Normalmente porque algo lo abrió antes que la persona. Algunos servicios de correo abren los enlaces de un mensaje entrante para revisarlos, y Supabase pone como ejemplo Safe Links de Microsoft Defender for Office 365. Abrir un enlace de email de Supabase es usarlo, así que cuando la persona hace clic un minuto después el token ya no está y Supabase responde \"Email link is invalid or has expired\".",{"q":639,"a":640},"¿Está confirmada la cuenta aunque el enlace dijera que había caducado?","Si un filtro de correo abrió primero un enlace de confirmación de registro, sí. Supabase confirma la dirección para quien abra el enlace, así que la visita del filtro la confirmó. Mira email_confirmed_at en auth.users: si tiene valor, la persona puede iniciar sesión con la contraseña que eligió.",{"q":642,"a":643},"¿Cuánto tiempo es válido un enlace de email de Supabase?","Una hora por defecto. Lo controla el ajuste Email OTP Expiration, y ese mismo ajuste cubre los magic links, las confirmaciones de registro, los restablecimientos de contraseña, los cambios de email y las invitaciones. Supabase desaconseja expresamente cualquier valor por encima de 86.400 segundos, es decir, un día.",{"q":645,"a":646},"¿Debería hacer que los enlaces duren más?","No. Un filtro de correo abre el enlace en cuanto llega el mensaje, así que una validez más larga no impide que lo use primero. Lo que sí hace es mantener funcionando más tiempo cada enlace sin usar en cada bandeja de entrada, y un enlace inicia la sesión de quien lo abra, incluida una persona a la que se le reenvió el email.",{"q":648,"a":649},"¿Por qué el enlace funciona en mi ordenador y falla en mi móvil?","Si tu app usa el flujo PKCE, el navegador donde la persona pidió el enlace guarda la mitad de lo que necesita el inicio de sesión, así que abrir el email en otro dispositivo falla con \"PKCE code verifier not found in storage\". Ese intento además gasta el enlace, y por eso volver a hacer clic en el primer dispositivo dice después \"Email link is invalid or has expired\".",{"q":651,"a":652},"¿Qué significa error_code=otp_expired en la dirección de mi app?","Es la forma en que Supabase Auth informa de un enlace de email fallido. Envía a la persona a tu app con error=access_denied, error_code=otp_expired y el mensaje en la parte de la dirección que va detrás del signo #. Tu app puede leer error_code ahí y mostrar una página que ofrezca enviar un enlace nuevo.","\u002Fblog\u002Femail-link-is-invalid-or-has-expired\u002Fcard-800x500.png",[655,656,657,658,659,660,74],"email link is invalid or has expired supabase","email link is invalid or has expired","otp_expired supabase","supabase magic link caducado al instante","enlace de confirmación supabase no funciona","outlook safe links magic link",{},true,"\"Email link is invalid or has expired\" en Supabase","\u002Fblog\u002Femail-link-is-invalid-or-has-expired","2026-11-01",{"title":5,"description":631},"blog\u002Femail-link-is-invalid-or-has-expired",[669,670,671],"Supabase muestra \"Email link is invalid or has expired\" cuando el token de un enlace de inicio de sesión o de confirmación ya se usó, fue sustituido por un email más reciente o tiene más de una hora. El código de error es otp_expired.","Con un enlace que la persona acaba de recibir, la causa habitual es un filtro de correo que abrió el enlace para revisarlo y lo gastó al hacerlo. La propia página de solución de problemas de Supabase lo llama el motivo más común.","Alargar la validez no cambia nada. Cambia el email para que abrir la dirección no haga nada: envía un código para escribir, o enlaza a una página donde un botón inicia la sesión.","ThS-DJAvT3JCYOuKRxtJy-x3cwLs8sSnfxlQVHfacNw",[674,680,686,692,698,703,709,715,720,721,727,734,740,745,751,757,763,769,775,781,787,792,797,803,809,815,821,827,833,839,845,851,857,863,869,875,881,887,893,899,905,911,917,923,929,935,940,946,952,958,964,970,976,982,988,994,1000,1006,1012,1018,1024,1030,1036,1042,1048,1054,1060,1066,1072,1078,1084,1090,1096,1102,1108,1114,1120,1126,1132,1138,1144,1150,1156,1162,1167,1173,1179,1185,1190,1196],{"path":675,"title":676,"description":677,"published":678,"category":628,"image":679,"draft":632},"\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":681,"title":682,"description":683,"published":684,"category":628,"image":685,"draft":632},"\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":687,"title":688,"description":689,"published":690,"category":628,"image":691,"draft":632},"\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":693,"title":694,"description":695,"published":696,"category":628,"image":697,"draft":632},"\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":82,"title":699,"description":700,"published":701,"category":628,"image":702,"draft":632},"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":704,"title":705,"description":706,"published":707,"category":628,"image":708,"draft":632},"\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":710,"title":711,"description":712,"published":713,"category":628,"image":714,"draft":632},"\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":453,"title":716,"description":717,"published":718,"category":628,"image":719,"draft":632},"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":664,"title":5,"description":631,"published":665,"category":628,"image":653,"draft":632},{"path":722,"title":723,"description":724,"published":725,"category":628,"image":726,"draft":632},"\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":728,"title":729,"description":730,"published":731,"category":732,"image":733,"draft":632},"\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":735,"title":736,"description":737,"published":738,"category":628,"image":739,"draft":632},"\u002Fblog\u002Fstripe-webhook-no-signatures-found","\"No signatures found matching the expected signature\" en Supabase","¿Tu webhook de Stripe en una Edge Function de Supabase dice \"No signatures found matching the expected signature\"? Tiene dos causas. Sigue verificando.","2026-10-29","\u002Fblog\u002Fstripe-webhook-no-signatures-found\u002Fcard-800x500.png",{"path":326,"title":741,"description":742,"published":743,"category":628,"image":744,"draft":632},"\"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":746,"title":747,"description":748,"published":749,"category":628,"image":750,"draft":632},"\u002Fblog\u002Fedge-function-returned-a-non-2xx-status-code","\"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":752,"title":753,"description":754,"published":755,"category":628,"image":756,"draft":632},"\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":758,"title":759,"description":760,"published":761,"category":628,"image":762,"draft":632},"\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":764,"title":765,"description":766,"published":767,"category":628,"image":768,"draft":632},"\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":770,"title":771,"description":772,"published":773,"category":628,"image":774,"draft":632},"\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":776,"title":777,"description":778,"published":779,"category":628,"image":780,"draft":632},"\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":782,"title":783,"description":784,"published":785,"category":628,"image":786,"draft":632},"\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":175,"title":788,"description":789,"published":790,"category":628,"image":791,"draft":632},"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":90,"title":793,"description":794,"published":795,"category":628,"image":796,"draft":632},"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":798,"title":799,"description":800,"published":801,"category":628,"image":802,"draft":632},"\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":804,"title":805,"description":806,"published":807,"category":628,"image":808,"draft":632},"\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":810,"title":811,"description":812,"published":813,"category":628,"image":814,"draft":632},"\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":816,"title":817,"description":818,"published":819,"category":628,"image":820,"draft":632},"\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":822,"title":823,"description":824,"published":825,"category":628,"image":826,"draft":632},"\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":828,"title":829,"description":830,"published":831,"category":628,"image":832,"draft":632},"\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":834,"title":835,"description":836,"published":837,"category":628,"image":838,"draft":632},"\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":840,"title":841,"description":842,"published":843,"category":628,"image":844,"draft":632},"\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":846,"title":847,"description":848,"published":849,"category":628,"image":850,"draft":632},"\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":852,"title":853,"description":854,"published":855,"category":628,"image":856,"draft":632},"\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":858,"title":859,"description":860,"published":861,"category":628,"image":862,"draft":632},"\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":864,"title":865,"description":866,"published":867,"category":628,"image":868,"draft":632},"\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":870,"title":871,"description":872,"published":873,"category":628,"image":874,"draft":632},"\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":876,"title":877,"description":878,"published":879,"category":628,"image":880,"draft":632},"\u002Fblog\u002Fmove-a-secret-to-an-edge-function","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":882,"title":883,"description":884,"published":885,"category":628,"image":886,"draft":632},"\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":888,"title":889,"description":890,"published":891,"category":628,"image":892,"draft":632},"\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":894,"title":895,"description":896,"published":897,"category":628,"image":898,"draft":632},"\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":900,"title":901,"description":902,"published":903,"category":628,"image":904,"draft":632},"\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":906,"title":907,"description":908,"published":909,"category":628,"image":910,"draft":632},"\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":912,"title":913,"description":914,"published":915,"category":628,"image":916,"draft":632},"\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":918,"title":919,"description":920,"published":921,"category":732,"image":922,"draft":632},"\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":924,"title":925,"description":926,"published":927,"category":732,"image":928,"draft":632},"\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":930,"title":931,"description":932,"published":933,"category":732,"image":934,"draft":632},"\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":936,"title":937,"description":938,"published":933,"category":628,"image":939,"draft":632},"\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":941,"title":942,"description":943,"published":944,"category":628,"image":945,"draft":632},"\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":947,"title":948,"description":949,"published":950,"category":628,"image":951,"draft":632},"\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":953,"title":954,"description":955,"published":956,"category":732,"image":957,"draft":632},"\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":959,"title":960,"description":961,"published":962,"category":628,"image":963,"draft":632},"\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":965,"title":966,"description":967,"published":968,"category":628,"image":969,"draft":632},"\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":971,"title":972,"description":973,"published":974,"category":628,"image":975,"draft":632},"\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":977,"title":978,"description":979,"published":980,"category":628,"image":981,"draft":632},"\u002Fblog\u002Fstripe-secret-key-in-frontend","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":983,"title":984,"description":985,"published":986,"category":628,"image":987,"draft":632},"\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":989,"title":990,"description":991,"published":992,"category":628,"image":993,"draft":632},"\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":995,"title":996,"description":997,"published":998,"category":628,"image":999,"draft":632},"\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":1001,"title":1002,"description":1003,"published":1004,"category":628,"image":1005,"draft":632},"\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":1007,"title":1008,"description":1009,"published":1010,"category":732,"image":1011,"draft":632},"\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":1013,"title":1014,"description":1015,"published":1016,"category":732,"image":1017,"draft":632},"\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":1019,"title":1020,"description":1021,"published":1022,"category":732,"image":1023,"draft":632},"\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":1025,"title":1026,"description":1027,"published":1028,"category":628,"image":1029,"draft":632},"\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":1031,"title":1032,"description":1033,"published":1034,"category":628,"image":1035,"draft":632},"\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":1037,"title":1038,"description":1039,"published":1040,"category":628,"image":1041,"draft":632},"\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":1043,"title":1044,"description":1045,"published":1046,"category":628,"image":1047,"draft":632},"\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":1049,"title":1050,"description":1051,"published":1052,"category":628,"image":1053,"draft":632},"\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":1055,"title":1056,"description":1057,"published":1058,"category":628,"image":1059,"draft":632},"\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":1061,"title":1062,"description":1063,"published":1064,"category":628,"image":1065,"draft":632},"\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":1067,"title":1068,"description":1069,"published":1070,"category":628,"image":1071,"draft":632},"\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":1073,"title":1074,"description":1075,"published":1076,"category":628,"image":1077,"draft":632},"\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":1079,"title":1080,"description":1081,"published":1082,"category":628,"image":1083,"draft":632},"\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":1085,"title":1086,"description":1087,"published":1088,"category":732,"image":1089,"draft":632},"\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":1091,"title":1092,"description":1093,"published":1094,"category":628,"image":1095,"draft":632},"\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":1097,"title":1098,"description":1099,"published":1100,"category":732,"image":1101,"draft":632},"\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":1103,"title":1104,"description":1105,"published":1106,"category":628,"image":1107,"draft":632},"\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":1109,"title":1110,"description":1111,"published":1112,"category":628,"image":1113,"draft":632},"\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":1115,"title":1116,"description":1117,"published":1118,"category":628,"image":1119,"draft":632},"\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":1121,"title":1122,"description":1123,"published":1124,"category":628,"image":1125,"draft":632},"\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":1127,"title":1128,"description":1129,"published":1130,"category":732,"image":1131,"draft":632},"\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":1133,"title":1134,"description":1135,"published":1136,"category":732,"image":1137,"draft":632},"\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":1139,"title":1140,"description":1141,"published":1142,"category":628,"image":1143,"draft":632},"\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":1145,"title":1146,"description":1147,"published":1148,"category":628,"image":1149,"draft":632},"\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":1151,"title":1152,"description":1153,"published":1154,"category":732,"image":1155,"draft":632},"\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":1157,"title":1158,"description":1159,"published":1160,"category":628,"image":1161,"draft":632},"\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":1163,"title":1164,"description":1165,"published":1160,"category":628,"image":1166,"draft":632},"\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":1168,"title":1169,"description":1170,"published":1171,"category":628,"image":1172,"draft":632},"\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":1174,"title":1175,"description":1176,"published":1177,"category":732,"image":1178,"draft":632},"\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":1180,"title":1181,"description":1182,"published":1183,"category":628,"image":1184,"draft":632},"\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":1186,"title":1187,"description":1188,"published":1183,"category":732,"image":1189,"draft":632},"\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":1191,"title":1192,"description":1193,"published":1194,"category":732,"image":1195,"draft":632},"\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":374,"title":1197,"description":1198,"published":1194,"category":628,"image":1199,"draft":632},"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",1794213644922]