[{"data":1,"prerenderedAt":839},["ShallowReactive",2],{"blog-es-vite-and-next-public-env-vars":3,"blog-index-es":598},{"id":4,"title":5,"body":6,"category":550,"cover":551,"coverAlt":552,"description":553,"draft":554,"extension":555,"faq":556,"image":575,"keywords":576,"meta":586,"navigation":587,"ogTitle":588,"path":589,"published":590,"seo":591,"stem":592,"tldr":593,"updated":590,"__hash__":597},"blog_es\u002Fblog\u002Fvite-and-next-public-env-vars.md","Variables de entorno de Vite expuestas: el prefijo dice publica esto",{"type":7,"value":8,"toc":536},"minimark",[9,25,40,43,48,54,72,86,93,99,105,121,127,133,137,140,151,154,160,164,167,318,324,333,337,340,360,371,375,378,398,408,413,420,424,427,434,455,463,471,475,518,521,528],[10,11,12,13,17,18,21,22,24],"p",{},"Alguien abrió tu app de Lovable, pulsó F12 y encontró en el código un valor\nque estás seguro de haber puesto en un archivo ",[14,15,16],"code",{},".env",". O le pediste al builder\nuna función, escribió una línea que empieza por ",[14,19,20],{},"VITE_",", y algo que has leído\ndesde entonces dice que ese prefijo es por donde se filtran las claves. El\narchivo lleva ",[14,23,16],{}," en el nombre y todos los tutoriales dicen que nunca se\ncomparte. Unas variables de entorno de Vite expuestas a cada visitante suenan\na fallo de Vite.",[10,26,27,28,32,33,35,36,39],{},"Aquí está la parte que una guía tras otra explica mal: ",[29,30,31],"strong",{},"nada falló al\nesconder ese valor."," ",[14,34,20],{}," y ",[14,37,38],{},"NEXT_PUBLIC_"," son una instrucción para tu\nherramienta de compilación, y la instrucción es meter el valor en la app que\ndescarga cada visitante. La herramienta leyó la etiqueta e hizo lo que decía.",[10,41,42],{},"Una compilación empaqueta tu app en una caja, y cada visitante recibe una\ncopia de la caja. El prefijo es una etiqueta en un valor que dice: mete esto\ntambién. El resto de este artículo trata de para qué valores esa etiqueta es\ncorrecta, por qué la IA se la pone a los equivocados, y cómo ver qué hay en tu\npropia caja.",[44,45,47],"h2",{"id":46},"las-variables-de-entorno-de-vite-están-expuestas-a-los-visitantes","¿Las variables de entorno de Vite están expuestas a los visitantes?",[10,49,50,51,53],{},"Las que empiezan por ",[14,52,20],{},", sí, y para eso existe el prefijo.",[10,55,56,57,59,60,63,64,67,68,71],{},"Vite, la herramienta de compilación detrás de la mayoría de las apps de\nLovable y Bolt, lee tu archivo ",[14,58,16],{}," cada vez que compila. Una variable\nllamada ",[14,61,62],{},"VITE_SUPABASE_URL"," se copia en el bundle, el archivo JavaScript\ncomprimido que descarga cada visitante, donde tu código la lee como\n",[14,65,66],{},"import.meta.env.VITE_SUPABASE_URL",". Una variable llamada ",[14,69,70],{},"DB_PASSWORD",", sin\nprefijo, llega vacía al navegador. La propia documentación de Vite dice que\nlos valores con prefijo se empaquetan en tu código fuente al compilar y no\ndeberían contener claves de API.",[10,73,74,75,77,78,81,82,85],{},"Next.js, con el que compila v0, tiene la misma regla con otro nombre:\n",[14,76,38],{},". Su documentación describe el valor como inlined, una cadena\nfija escrita en el bundle del navegador al compilar. Expo usa ",[14,79,80],{},"EXPO_PUBLIC_"," y\navisa con las mismas palabras. Los proyectos antiguos de Create React App usan\n",[14,83,84],{},"REACT_APP_",". Cada prefijo le dice lo mismo a su herramienta: este va en la\ncaja.",[44,87,89,90,92],{"id":88},"por-qué-un-archivo-env-parece-privado-y-no-lo-es","Por qué un archivo ",[14,91,16],{}," parece privado y no lo es",[10,94,95,96,98],{},"Porque la costumbre de guardar una clave en un archivo ",[14,97,16],{}," viene de los\nservidores, donde funciona.",[10,100,101,102,104],{},"En un servidor, el archivo y el código que lo lee están en una máquina que tú\ncontrolas. Un visitante recibe una respuesta de esa máquina y nunca ve el\narchivo. Sacar la clave del código y ponerla en un archivo que el servidor lee\nal arrancar es buena práctica allí, y allí se escribió cada tutorial que dice\n«ponla en ",[14,103,16],{},"».",[10,106,107,108,110,111,114,115,117,118,120],{},"Tu archivo ",[14,109,16],{}," tiene dos lectores, y los tutoriales hablan de uno. El\nprimero es cualquiera que pueda ver tu proyecto: un colaborador, un\nrepositorio público de GitHub, la vista de archivos del propio builder. Una\nentrada en ",[14,112,113],{},".gitignore",", que es una lista de archivos que git deja fuera,\nmantiene ",[14,116,16],{}," lejos de ese lector. El segundo lector es la compilación, que\nabre el archivo cada vez que publicas y copia todo lo que lleve el prefijo.\n",[14,119,113],{}," no le dice nada.",[122,123],"diagram",{"alt":124,"caption":125,"src":126},"Un archivo .env en el centro con tres valores, dos de ellos marcados VITE_. A su izquierda, un muro discontinuo con la etiqueta .gitignore corta el camino hacia una pila de archivos del repositorio. A su derecha, los dos valores marcados viajan por un paso de compilación hasta una ventana de navegador con una persona al lado, y el valor sin marcar se queda en el archivo.","Un archivo, dos lectores. El muro de .gitignore detiene a uno. La compilación lleva cada valor con prefijo al otro.","\u002Fblog\u002Fvite-and-next-public-env-vars\u002Ftwo-readers-one-file-1600x760.png",[10,128,129,130,132],{},"Así que «mi ",[14,131,16],{}," está en gitignore» es cierto, y responde a otra pregunta.\nEl archivo se quedó fuera de tu repositorio. Los valores con prefijo entraron\nen la app de todos modos, porque esa es la ruta que abre el prefijo, y en un\nproyecto de Lovable, Bolt o v0 la mayor parte del código que has estado\neditando corre en el navegador, donde no hay servidor tras el que el archivo\npueda quedarse.",[44,134,136],{"id":135},"por-qué-la-ia-recurrió-al-prefijo","Por qué la IA recurrió al prefijo",[10,138,139],{},"Porque así es como se hace funcionar un valor en código del navegador, y el\nmodelo no tiene ni idea de lo que cuesta ese valor.",[10,141,142,143,146,147,150],{},"Pediste un mapa, o un chat que responda preguntas sobre tu producto. El código\nque el builder escribió para eso corre en el navegador del visitante, y el\ncódigo del navegador que lee ",[14,144,145],{},"process.env.OPENAI_API_KEY"," no recibe nada. La\nforma de hacer que el valor llegue es el prefijo. El builder renombra la\nvariable a ",[14,148,149],{},"VITE_OPENAI_API_KEY",", la función funciona en la vista previa y no\nsalta ningún error en ningún sitio, porque desde el lado de la herramienta de\ncompilación nada salió mal.",[10,152,153],{},"El prefijo no juzga. Es la misma instrucción para una clave de Google Maps,\nque debe ser pública una vez restringida, y para una clave secreta de Stripe,\nque puede reembolsar a cada cliente que tienes. Una persona que supiera que la\nsegunda carga a tu tarjeta se pararía. El modelo sabe que la función no\nfuncionaba hasta que la línea estuvo ahí, y después sí.",[10,155,156,157,159],{},"Por eso esto aparece en apps cuyos dueños hicieron todo lo que les dijeron. El\nvalor se sacó del código, se guardó en ",[14,158,16],{},", el archivo estaba en gitignore,\ny la app lo envía de todos modos, porque el único paso que lo publica se\nparece al paso que lo hace funcionar.",[44,161,163],{"id":162},"qué-valores-van-detrás-del-prefijo","Qué valores van detrás del prefijo",[10,165,166],{},"Una dirección y una clave publicable. Cualquier cosa que gaste dinero o ignore\nlas reglas de tu base de datos, no.",[168,169,170,192],"table",{},[171,172,173],"thead",{},[174,175,176,180,189],"tr",{},[177,178,179],"th",{},"Valor",[177,181,182,183,185,186,188],{},"¿Detrás de ",[14,184,20],{}," o ",[14,187,38],{},"?",[177,190,191],{},"Por qué",[193,194,195,212,231,247,260,274,292,306],"tbody",{},[174,196,197,202,209],{},[198,199,200],"td",{},[14,201,62],{},[198,203,204],{},[205,206,208],"key-verdict",{"type":207},"safe","Va aquí",[198,210,211],{},"Una dirección. Dice con qué proyecto habla tu app y nada más.",[174,213,214,224,228],{},[198,215,216,219,220,223],{},[14,217,218],{},"VITE_SUPABASE_PUBLISHABLE_KEY",", o ",[14,221,222],{},"VITE_SUPABASE_ANON_KEY"," en un proyecto antiguo",[198,225,226],{},[205,227,208],{"type":207},[198,229,230],{},"Diseñada para el navegador. Cada petición que hace sigue filtrada por Row Level Security, las reglas de cada tabla que deciden fila a fila quién puede leer qué.",[174,232,233,240,244],{},[198,234,235,236,239],{},"Una clave ",[14,237,238],{},"pk_live_"," de Stripe",[198,241,242],{},[205,243,208],{"type":207},[198,245,246],{},"Construye formularios de pago. No puede cobrar, reembolsar ni leer clientes.",[174,248,249,252,257],{},[198,250,251],{},"Una clave de Google Maps",[198,253,254],{},[205,255,256],{"type":207},"Va aquí, una vez restringida",[198,258,259],{},"Pública por diseño. Una restricción por referrer en Google Cloud es lo que impide que un desconocido te facture con ella.",[174,261,262,265,271],{},[198,263,264],{},"Una clave de OpenAI o Anthropic",[198,266,267],{},[205,268,270],{"type":269},"danger","Nunca",[198,272,273],{},"No existe variante publicable. Quien la tenga gasta tu dinero.",[174,275,276,285,289],{},[198,277,235,278,185,281,284],{},[14,279,280],{},"service_role",[14,282,283],{},"sb_secret_"," de Supabase",[198,286,287],{},[205,288,270],{"type":269},[198,290,291],{},"Se salta Row Level Security y lee cada fila de cada tabla.",[174,293,294,299,303],{},[198,295,235,296,239],{},[14,297,298],{},"sk_live_",[198,300,301],{},[205,302,270],{"type":269},[198,304,305],{},"Cobros, reembolsos, pagos y cada registro de cliente.",[174,307,308,311,315],{},[198,309,310],{},"Una clave de acceso de AWS",[198,312,313],{},[205,314,270],{"type":269},[198,316,317],{},"Todo lo que pueda hacer esa cuenta, desde cualquier sitio.",[10,319,320,321,323],{},"Si tu app tiene ",[14,322,62],{}," y una clave publicable al lado, ese es el\npar que Supabase pensó para un navegador, y nuestro escaneo lo marca como que\nva ahí. La dirección y la clave publicable son la razón de que exista el\nprefijo.",[10,325,326,327,332],{},"La prueba para todo lo demás es si te importaría ver el valor impreso en tu\npágina de inicio. El prefijo lo deja un clic más lejos que eso, en un archivo\nen vez de en la página, y quien quiera el archivo lo tiene.\n",[328,329,331],"a",{"href":330},"\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend","Distinguir las dos familias",",\npor el prefijo y, en una clave antigua de Supabase, por el rol que lleva\ndentro, es un artículo aparte.",[44,334,336],{"id":335},"qué-enviaban-30998-apps-detrás-del-prefijo","Qué enviaban 30.998 apps detrás del prefijo",[10,338,339],{},"Sobre todo claves de API de Google. 52 apps enviaban una clave que gasta dinero\no lo lee todo.",[10,341,342,343,347,348,352,353,185,356,359],{},"En agosto de 2026 ejecutamos\n",[328,344,346],{"href":345},"\u002Fresearch\u002Fvibe-coded-app-security-2026","las mismas nueve comprobaciones","\nsobre 30.998 apps vibe-coded en producción. 1.332 de ellas, el 4 %, enviaban\nalgo con forma de clave en el código que descarga cada visitante. 1.142 de\nesas eran claves de API de Google, que normalmente necesitan una\n",[328,349,351],{"href":350},"\u002Fblog\u002Fgoogle-api-key-exposed-in-frontend","restricción en Google Cloud"," y no\nuna rotación. 204 enviaban un valor de aspecto aleatorio junto a un nombre\ncomo ",[14,354,355],{},"secret",[14,357,358],{},"password",", que puede ser una credencial real o no.",[10,361,362,363,365,366,370],{},"Las caras eran raras. 33 apps enviaban una clave de OpenAI, 9 una clave de\nacceso de AWS, 5 una de Anthropic, 3 una clave secreta de Stripe y 3 una\n",[14,364,280],{}," de Supabase: 52 apps en total, porque una de ellas llevaba dos.\nAsí que el valor detrás del prefijo suele ser una clave de Google, y la\nsolución para eso es un ajuste. El caso raro es donde está el daño, y\n",[328,367,369],{"href":368},"\u002Fblog\u002Fopenai-api-key-exposed-in-frontend","lo que cuesta una clave de OpenAI filtrada","\nes el artículo para ese.",[44,372,374],{"id":373},"cómo-comprobar-qué-envía-tu-propia-app","Cómo comprobar qué envía tu propia app",[10,376,377],{},"Abre la app en producción, pulsa F12 y busca el valor en cada archivo cargado.",[379,380,381,385,392,395],"ol",{},[382,383,384],"li",{},"Abre tu app publicada en un navegador, en su dirección real. La vista previa\ndel builder es otra compilación y puede ir una versión por detrás.",[382,386,387,388,391],{},"Pulsa F12 para abrir las herramientas de desarrollo y elige la pestaña\n",[29,389,390],{},"Sources",".",[382,393,394],{},"Pulsa Ctrl+Mayús+F, o Cmd+Opción+F en un Mac. Eso abre una búsqueda en\ncada archivo que cargó la página.",[382,396,397],{},"Pega los primeros diez caracteres, más o menos, del valor que te preocupa,\ny no lo pegues en ningún otro sitio.",[10,399,400,401,404,405,407],{},"Un resultado significa que el valor está en la caja que recibe cada visitante.\nBusca el valor y no el nombre: la compilación suele sustituir\n",[14,402,403],{},"import.meta.env.VITE_OPENAI_API_KEY"," por el valor en sí, así que una búsqueda\nde ",[14,406,20],{}," puede volver vacía mientras cada valor que había detrás está ahí.",[122,409],{"alt":410,"caption":411,"src":412},"Dos paneles. A la izquierda, un archivo fuente con una línea que dice import.meta.env.VITE_OPENAI_API_KEY entre líneas de código lisas. A la derecha, tras un paso de compilación, el mismo archivo comprimido en ruido denso, con el nombre de la variable desaparecido y su valor dibujado como una barra resaltada bajo una lupa.","La compilación sustituye el nombre por el valor. Buscar VITE_ en la app en producción no encuentra nada; buscar el valor lo encuentra.","\u002Fblog\u002Fvite-and-next-public-env-vars\u002Fname-goes-value-stays-1600x640.png",[10,414,415,416,391],{},"Si prefieres no recorrer tu app valor por valor, nuestro escaneo gratuito lee\ntu sitio en producción desde fuera, las nueve comprobaciones, en unos 20\nsegundos y sin cuenta. Nombra cada clave que encuentra por su tipo, dice\ncuáles van en un navegador, y escribe «No se pudo comprobar» para todo lo que\nno pudo responder en lugar de una marca de visto:\n",[328,417,419],{"href":418},"\u002Fsecurity-scanner","escanea tu app",[44,421,423],{"id":422},"dónde-va-un-secreto-de-verdad","Dónde va un secreto de verdad",[10,425,426],{},"En una máquina de la que tus visitantes nunca descargan nada. En un proyecto\nde Supabase es una Edge Function, un pequeño trozo de código de servidor que\nSupabase ejecuta por ti; en una app de Next.js es una ruta de servidor; en una\napp de Replit es la mitad del servidor.",[10,428,429,430,433],{},"La forma es la misma en todas partes. Tu código del navegador le pide a tu\nfunción que haga el trabajo. La función guarda la clave, hace la llamada a\nOpenAI o Stripe y devuelve la respuesta. La clave se queda en la máquina y el\nvisitante recibe un resultado. El\n",[328,431,432],{"href":368},"artículo sobre OpenAI"," lo dibuja\ncomo un objeto que se mueve una caja a la derecha, y el cambio es solo eso.",[10,435,436,437,440,441,443,444,447,448,451,452,391],{},"Dos cosas te dicen que el builder hizo lo que pediste. La variable perdió su\nprefijo, así que es ",[14,438,439],{},"OPENAI_API_KEY",", y vive en los secretos de la propia\nfunción, definidos en el panel de Supabase bajo Edge Functions, sin nada en el\n",[14,442,16],{}," de la app. Y el archivo que la lee está bajo ",[14,445,446],{},"supabase\u002Ffunctions\u002F"," o\n",[14,449,450],{},"app\u002Fapi\u002F",", en algún sitio que la compilación nunca empaqueta, en vez de bajo\n",[14,453,454],{},"src\u002F",[10,456,457,458,462],{},"Un orden importa, y es fácil hacerlo al revés. Si una clave secreta ya se\nenvió detrás de un prefijo, moverla no hace nada por las copias ya\ndescargadas. Rótala primero en el proveedor, y después mueve el trabajo.\n",[328,459,461],{"href":460},"\u002Fblog\u002Frotate-supabase-service-role-key","Si rotar primero o cerrar primero la fuga","\ndepende de si la copia ya es pública, y una vez que estuvo en un bundle\npublicado, lo es.",[10,464,465,466,470],{},"En un proyecto de Replit la misma separación tiene nombre propio, Secrets, y\n",[328,467,469],{"href":468},"\u002Fblog\u002Freplit-secrets-explained","lo que cubre la herramienta Secrets y lo que no","\nes un artículo aparte.",[44,472,474],{"id":473},"qué-hacer-ahora-mismo","Qué hacer ahora mismo",[476,477,478],"key-takeaways",{},[479,480,481,494,506,509,512,515],"ul",{},[382,482,483,484,486,487,486,489,35,491,493],{},"Busca en tu proyecto ",[14,485,20],{},", ",[14,488,38],{},[14,490,80],{},[14,492,84],{},". Cada coincidencia es un valor que tu compilación publica a propósito. Decide para cada uno si puede ser público.",[382,495,496,497,35,499,501,502,505],{},"Conserva la dirección y la clave publicable. ",[14,498,62],{},[14,500,218],{},", o la clave ",[14,503,504],{},"anon"," en un proyecto antiguo, son el par para el que existe el prefijo.",[382,507,508],{},"Una clave secreta detrás de un prefijo se rota primero en el proveedor y luego se mueve. Borrar la línea no recupera una copia que ya se descargó.",[382,510,511],{},"Mueve el trabajo que necesitaba la clave a una Edge Function o una ruta de servidor, con la clave en los secretos de esa función y sin prefijo en su nombre.",[382,513,514],{},"Restringe una clave de Google por referrer en Google Cloud. Esa necesita un ajuste y conserva su valor.",[382,516,517],{},"Después de la siguiente publicación, busca en la app en producción cada valor que moviste.",[10,519,520],{},"Cada publicación empaqueta una caja nueva. La siguiente función que pidas es\notra oportunidad de que un valor reciba el prefijo, y nada entre el builder e\ninternet lee el bundle de camino a la salida. Un escaneo del mes pasado leyó\nel bundle del mes pasado.",[10,522,523,527],{},[328,524,526],{"href":525},"\u002Fpricing","Reeve Monitor"," lee el bundle por ti. Vuelve a ejecutar las nueve\ncomprobaciones cada hora en hasta tres apps, te avisa cuando un resultado\ncambia, vigila la disponibilidad cada 60 segundos y envía un informe mensual.\nUna clave que llega al bundle con la publicación del martes aparece en el\nreescaneo de esa hora, te acordaras de mirar o no. Cuesta $12 al mes a precio\nde lista, con siete días gratis antes de cobrarte; la página de precios a\nveces está por debajo de la cifra de aquí y nunca por encima.",[10,529,530,531,535],{},"Si prefieres recorrer esto como una lista, la\n",[328,532,534],{"href":533},"\u002Fchecklist","lista de seguridad de 10 minutos"," cubre esto y las demás cosas\nque conviene desactivar en una app recién lanzada.",{"title":537,"searchDepth":538,"depth":538,"links":539},"",3,[540,542,544,545,546,547,548,549],{"id":46,"depth":541,"text":47},2,{"id":88,"depth":541,"text":543},"Por qué un archivo .env parece privado y no lo es",{"id":135,"depth":541,"text":136},{"id":162,"depth":541,"text":163},{"id":335,"depth":541,"text":336},{"id":373,"depth":541,"text":374},{"id":422,"depth":541,"text":423},{"id":473,"depth":541,"text":474},"Fundamentos de seguridad","\u002Fblog\u002Fvite-and-next-public-env-vars\u002Fcover-1200x630.png","Un archivo .env con tres valores, dos con la etiqueta VITE_, y esos dos valores dibujados de nuevo en una ventana de navegador al lado.","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.",false,"md",[557,560,563,566,569,572],{"q":558,"a":559},"¿Los archivos .env son secretos?","Para tu repositorio, sí, si el archivo está en .gitignore. Para tus visitantes, no. La compilación lee .env cada vez que publicas y copia cada valor con el prefijo VITE_ o NEXT_PUBLIC_ en el JavaScript que envía tu app. El archivo en sí nunca sale de tu máquina; los valores que etiquetaste para el navegador, sí.",{"q":561,"a":562},"¿Es seguro exponer VITE_SUPABASE_ANON_KEY?","Está pensada para estar ahí. La clave anon, llamada clave publicable en un proyecto nuevo, está diseñada para vivir en un navegador. Solo dice a qué proyecto pertenece una petición, y cada petición que hace pasa por el filtro de tus reglas de Row Level Security. Eso se cumple exactamente mientras esas reglas estén activadas y sean correctas, que es otra cosa a comprobar.",{"q":564,"a":565},"¿NEXT_PUBLIC_ es distinto de VITE_?","Misma regla, otra herramienta de compilación. Next.js escribe el valor de cualquier variable NEXT_PUBLIC_ en el bundle del navegador como una cadena fija al compilar, y una variable sin el prefijo llega vacía al código del navegador. Expo hace lo mismo con EXPO_PUBLIC_, y los proyectos antiguos de Create React App con REACT_APP_. Sea cual sea la herramienta que compiló tu app, el prefijo significa publicar.",{"q":567,"a":568},"¿Cómo compruebo qué hay en mi bundle?","Abre la app en producción, pulsa F12, elige Sources y pulsa Ctrl+Mayús+F (Cmd+Opción+F en un Mac) para buscar en cada archivo que cargó la página. Pega los primeros caracteres del valor. Busca el valor y no el nombre de la variable, porque la compilación suele sustituir el nombre por el valor, así que VITE_ puede no aparecer mientras la clave sí está. Nuestro escaneo gratuito hace la misma lectura desde fuera en unos 20 segundos.",{"q":570,"a":571},"¿Dónde debería vivir una clave secreta en una app de Lovable o Bolt?","En una Supabase Edge Function, con la clave guardada en los secretos de esa función en el panel de Supabase y sin el prefijo VITE_ en su nombre. Tu código del navegador llama a la función, la función llama al proveedor con la clave, y la clave nunca llega a un visitante. Si la clave ya se envió, rótala en el proveedor antes de moverla.",{"q":573,"a":574},"¿.gitignore protege mis claves?","Mantiene el archivo .env fuera de git, así que nadie que lea tu repositorio lo ve. No tiene ningún efecto sobre la compilación, que lee el archivo directamente y publica cada valor con prefijo. Un .env ignorado por git con VITE_OPENAI_API_KEY dentro sigue enviando esa clave a cada visitante.","\u002Fblog\u002Fvite-and-next-public-env-vars\u002Fcard-800x500.png",[577,578,579,580,581,582,583,584,585],"variables de entorno vite expuestas","prefijo VITE_ seguridad","next_public_ variable de entorno secreta","son seguras las variables .env en react","es seguro vite_","variables de entorno en el frontend","archivo .env expuesto en el navegador","expo_public env","ocultar api key en el frontend",{},true,"Variables de entorno de Vite expuestas: VITE_ dice publica esto","\u002Fblog\u002Fvite-and-next-public-env-vars","2026-09-18",{"title":5,"description":553},"blog\u002Fvite-and-next-public-env-vars",[594,595,596],"Las variables de entorno de Vite expuestas en tu app están ahí porque el prefijo lo pidió. Una variable llamada VITE_ o NEXT_PUBLIC_ se copia en el JavaScript que descarga cada visitante, y guardarla en un archivo .env no lo impide.","El prefijo es la etiqueta correcta para una dirección o una clave publicable, y la incorrecta para cualquier cosa que gaste dinero o ignore las reglas de tu base de datos. La compilación no distingue entre ambas, y la IA que escribió la línea tampoco.","De 30.998 apps vibe-coded en producción que escaneamos, 1.332 enviaban algo con forma de clave. 1.142 de ellas eran claves de API de Google, que normalmente necesitan una restricción y no una rotación. 52 enviaban una clave que gasta dinero o lo lee todo.","bsI2aB4vmYlx3Xf-B7egpLZR21ODJmk_sjH8YC6SV9c",[599,606,612,618,624,630,631,637,643,649,655,661,667,673,679,685,690,696,702,708,714,719,725,731,736,742,748,754,760,766,772,778,784,789,795,801,806,812,818,824,829,835],{"path":600,"title":601,"description":602,"published":603,"category":604,"image":605,"draft":554},"\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","Backups","\u002Fblog\u002Fsupabase-backup-auth-users\u002Fcard-800x500.png",{"path":607,"title":608,"description":609,"published":610,"category":550,"image":611,"draft":554},"\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":613,"title":614,"description":615,"published":616,"category":550,"image":617,"draft":554},"\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":619,"title":620,"description":621,"published":622,"category":550,"image":623,"draft":554},"\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":625,"title":626,"description":627,"published":628,"category":550,"image":629,"draft":554},"\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":589,"title":5,"description":553,"published":590,"category":550,"image":575,"draft":554},{"path":632,"title":633,"description":634,"published":635,"category":550,"image":636,"draft":554},"\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":638,"title":639,"description":640,"published":641,"category":550,"image":642,"draft":554},"\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":644,"title":645,"description":646,"published":647,"category":550,"image":648,"draft":554},"\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":650,"title":651,"description":652,"published":653,"category":604,"image":654,"draft":554},"\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":656,"title":657,"description":658,"published":659,"category":604,"image":660,"draft":554},"\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":662,"title":663,"description":664,"published":665,"category":604,"image":666,"draft":554},"\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":668,"title":669,"description":670,"published":671,"category":550,"image":672,"draft":554},"\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":674,"title":675,"description":676,"published":677,"category":550,"image":678,"draft":554},"\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":680,"title":681,"description":682,"published":683,"category":550,"image":684,"draft":554},"\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":460,"title":686,"description":687,"published":688,"category":550,"image":689,"draft":554},"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":691,"title":692,"description":693,"published":694,"category":550,"image":695,"draft":554},"\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. Diez herramientas, con precios.","2026-09-07","\u002Fblog\u002Fvibe-coding-security-scanners-compared\u002Fcard-800x500.png",{"path":697,"title":698,"description":699,"published":700,"category":550,"image":701,"draft":554},"\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":703,"title":704,"description":705,"published":706,"category":550,"image":707,"draft":554},"\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":709,"title":710,"description":711,"published":712,"category":550,"image":713,"draft":554},"\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":368,"title":715,"description":716,"published":717,"category":550,"image":718,"draft":554},"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":720,"title":721,"description":722,"published":723,"category":550,"image":724,"draft":554},"\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":726,"title":727,"description":728,"published":729,"category":604,"image":730,"draft":554},"\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":468,"title":732,"description":733,"published":734,"category":550,"image":735,"draft":554},"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":737,"title":738,"description":739,"published":740,"category":604,"image":741,"draft":554},"\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":743,"title":744,"description":745,"published":746,"category":550,"image":747,"draft":554},"\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":749,"title":750,"description":751,"published":752,"category":550,"image":753,"draft":554},"\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":755,"title":756,"description":757,"published":758,"category":550,"image":759,"draft":554},"\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":761,"title":762,"description":763,"published":764,"category":550,"image":765,"draft":554},"\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":767,"title":768,"description":769,"published":770,"category":604,"image":771,"draft":554},"\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":773,"title":774,"description":775,"published":776,"category":604,"image":777,"draft":554},"\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":779,"title":780,"description":781,"published":782,"category":550,"image":783,"draft":554},"\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":350,"title":785,"description":786,"published":787,"category":550,"image":788,"draft":554},"¿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":790,"title":791,"description":792,"published":793,"category":604,"image":794,"draft":554},"\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":796,"title":797,"description":798,"published":799,"category":550,"image":800,"draft":554},"\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":802,"title":803,"description":804,"published":799,"category":550,"image":805,"draft":554},"\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":807,"title":808,"description":809,"published":810,"category":550,"image":811,"draft":554},"\u002Fblog\u002Fsupabase-new-api-keys","Las nuevas claves de API de Supabase: ¿cuál va en tu app?","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":813,"title":814,"description":815,"published":816,"category":604,"image":817,"draft":554},"\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":819,"title":820,"description":821,"published":822,"category":550,"image":823,"draft":554},"\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":825,"title":826,"description":827,"published":822,"category":604,"image":828,"draft":554},"\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":830,"title":831,"description":832,"published":833,"category":604,"image":834,"draft":554},"\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":330,"title":836,"description":837,"published":833,"category":550,"image":838,"draft":554},"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",1790150954059]