Fundamentos de seguridad
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.

En resumen
- 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.
Alguien abrió tu app de Lovable, pulsó F12 y encontró en el código un valor
que estás seguro de haber puesto en un archivo .env. O le pediste al builder
una función, escribió una línea que empieza por VITE_, y algo que has leído
desde entonces dice que ese prefijo es por donde se filtran las claves. El
archivo lleva .env en el nombre y todos los tutoriales dicen que nunca se
comparte. Unas variables de entorno de Vite expuestas a cada visitante suenan
a fallo de Vite.
Aquí está la parte que una guía tras otra explica mal: nada falló al
esconder ese valor. VITE_ y NEXT_PUBLIC_ son una instrucción para tu
herramienta de compilación, y la instrucción es meter el valor en la app que
descarga cada visitante. La herramienta leyó la etiqueta e hizo lo que decía.
Una compilación empaqueta tu app en una caja, y cada visitante recibe una copia de la caja. El prefijo es una etiqueta en un valor que dice: mete esto también. El resto de este artículo trata de para qué valores esa etiqueta es correcta, por qué la IA se la pone a los equivocados, y cómo ver qué hay en tu propia caja.
¿Las variables de entorno de Vite están expuestas a los visitantes?
Las que empiezan por VITE_, sí, y para eso existe el prefijo.
Vite, la herramienta de compilación detrás de la mayoría de las apps de
Lovable y Bolt, lee tu archivo .env cada vez que compila. Una variable
llamada VITE_SUPABASE_URL se copia en el bundle, el archivo JavaScript
comprimido que descarga cada visitante, donde tu código la lee como
import.meta.env.VITE_SUPABASE_URL. Una variable llamada DB_PASSWORD, sin
prefijo, llega vacía al navegador. La propia documentación de Vite dice que
los valores con prefijo se empaquetan en tu código fuente al compilar y no
deberían contener claves de API.
Next.js, con el que compila v0, tiene la misma regla con otro nombre:
NEXT_PUBLIC_. Su documentación describe el valor como inlined, una cadena
fija escrita en el bundle del navegador al compilar. Expo usa EXPO_PUBLIC_ y
avisa con las mismas palabras. Los proyectos antiguos de Create React App usan
REACT_APP_. Cada prefijo le dice lo mismo a su herramienta: este va en la
caja.
Por qué un archivo .env parece privado y no lo es
Porque la costumbre de guardar una clave en un archivo .env viene de los
servidores, donde funciona.
En un servidor, el archivo y el código que lo lee están en una máquina que tú
controlas. Un visitante recibe una respuesta de esa máquina y nunca ve el
archivo. Sacar la clave del código y ponerla en un archivo que el servidor lee
al arrancar es buena práctica allí, y allí se escribió cada tutorial que dice
«ponla en .env».
Tu archivo .env tiene dos lectores, y los tutoriales hablan de uno. El
primero es cualquiera que pueda ver tu proyecto: un colaborador, un
repositorio público de GitHub, la vista de archivos del propio builder. Una
entrada en .gitignore, que es una lista de archivos que git deja fuera,
mantiene .env lejos de ese lector. El segundo lector es la compilación, que
abre el archivo cada vez que publicas y copia todo lo que lleve el prefijo.
.gitignore no le dice nada.
Así que «mi .env está en gitignore» es cierto, y responde a otra pregunta.
El archivo se quedó fuera de tu repositorio. Los valores con prefijo entraron
en la app de todos modos, porque esa es la ruta que abre el prefijo, y en un
proyecto de Lovable, Bolt o v0 la mayor parte del código que has estado
editando corre en el navegador, donde no hay servidor tras el que el archivo
pueda quedarse.
Por qué la IA recurrió al prefijo
Porque así es como se hace funcionar un valor en código del navegador, y el modelo no tiene ni idea de lo que cuesta ese valor.
Pediste un mapa, o un chat que responda preguntas sobre tu producto. El código
que el builder escribió para eso corre en el navegador del visitante, y el
código del navegador que lee process.env.OPENAI_API_KEY no recibe nada. La
forma de hacer que el valor llegue es el prefijo. El builder renombra la
variable a VITE_OPENAI_API_KEY, la función funciona en la vista previa y no
salta ningún error en ningún sitio, porque desde el lado de la herramienta de
compilación nada salió mal.
El prefijo no juzga. Es la misma instrucción para una clave de Google Maps, que debe ser pública una vez restringida, y para una clave secreta de Stripe, que puede reembolsar a cada cliente que tienes. Una persona que supiera que la segunda carga a tu tarjeta se pararía. El modelo sabe que la función no funcionaba hasta que la línea estuvo ahí, y después sí.
Por eso esto aparece en apps cuyos dueños hicieron todo lo que les dijeron. El
valor se sacó del código, se guardó en .env, el archivo estaba en gitignore,
y la app lo envía de todos modos, porque el único paso que lo publica se
parece al paso que lo hace funcionar.
Qué valores van detrás del prefijo
Una dirección y una clave publicable. Cualquier cosa que gaste dinero o ignore las reglas de tu base de datos, no.
| Valor | ¿Detrás de VITE_ o NEXT_PUBLIC_? | Por qué |
|---|---|---|
VITE_SUPABASE_URL | Va aquí | Una dirección. Dice con qué proyecto habla tu app y nada más. |
VITE_SUPABASE_PUBLISHABLE_KEY, o VITE_SUPABASE_ANON_KEY en un proyecto antiguo | Va aquí | 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é. |
Una clave pk_live_ de Stripe | Va aquí | Construye formularios de pago. No puede cobrar, reembolsar ni leer clientes. |
| Una clave de Google Maps | Va aquí, una vez restringida | Pública por diseño. Una restricción por referrer en Google Cloud es lo que impide que un desconocido te facture con ella. |
| Una clave de OpenAI o Anthropic | Nunca | No existe variante publicable. Quien la tenga gasta tu dinero. |
Una clave service_role o sb_secret_ de Supabase | Nunca | Se salta Row Level Security y lee cada fila de cada tabla. |
Una clave sk_live_ de Stripe | Nunca | Cobros, reembolsos, pagos y cada registro de cliente. |
| Una clave de acceso de AWS | Nunca | Todo lo que pueda hacer esa cuenta, desde cualquier sitio. |
Si tu app tiene VITE_SUPABASE_URL y una clave publicable al lado, ese es el
par que Supabase pensó para un navegador, y nuestro escaneo lo marca como que
va ahí. La dirección y la clave publicable son la razón de que exista el
prefijo.
La prueba para todo lo demás es si te importaría ver el valor impreso en tu página de inicio. El prefijo lo deja un clic más lejos que eso, en un archivo en vez de en la página, y quien quiera el archivo lo tiene. Distinguir las dos familias, por el prefijo y, en una clave antigua de Supabase, por el rol que lleva dentro, es un artículo aparte.
Qué enviaban 30.998 apps detrás del prefijo
Sobre todo claves de API de Google. 52 apps enviaban una clave que gasta dinero o lo lee todo.
En agosto de 2026 ejecutamos
las mismas nueve comprobaciones
sobre 30.998 apps vibe-coded en producción. 1.332 de ellas, el 4 %, enviaban
algo con forma de clave en el código que descarga cada visitante. 1.142 de
esas eran claves de API de Google, que normalmente necesitan una
restricción en Google Cloud y no
una rotación. 204 enviaban un valor de aspecto aleatorio junto a un nombre
como secret o password, que puede ser una credencial real o no.
Las caras eran raras. 33 apps enviaban una clave de OpenAI, 9 una clave de
acceso de AWS, 5 una de Anthropic, 3 una clave secreta de Stripe y 3 una
service_role de Supabase: 52 apps en total, porque una de ellas llevaba dos.
Así que el valor detrás del prefijo suele ser una clave de Google, y la
solución para eso es un ajuste. El caso raro es donde está el daño, y
lo que cuesta una clave de OpenAI filtrada
es el artículo para ese.
Cómo comprobar qué envía tu propia app
Abre la app en producción, pulsa F12 y busca el valor en cada archivo cargado.
- Abre tu app publicada en un navegador, en su dirección real. La vista previa del builder es otra compilación y puede ir una versión por detrás.
- Pulsa F12 para abrir las herramientas de desarrollo y elige la pestaña Sources.
- Pulsa Ctrl+Mayús+F, o Cmd+Opción+F en un Mac. Eso abre una búsqueda en cada archivo que cargó la página.
- Pega los primeros diez caracteres, más o menos, del valor que te preocupa, y no lo pegues en ningún otro sitio.
Un resultado significa que el valor está en la caja que recibe cada visitante.
Busca el valor y no el nombre: la compilación suele sustituir
import.meta.env.VITE_OPENAI_API_KEY por el valor en sí, así que una búsqueda
de VITE_ puede volver vacía mientras cada valor que había detrás está ahí.
Si prefieres no recorrer tu app valor por valor, nuestro escaneo gratuito lee tu sitio en producción desde fuera, las nueve comprobaciones, en unos 20 segundos y sin cuenta. Nombra cada clave que encuentra por su tipo, dice cuáles van en un navegador, y escribe «No se pudo comprobar» para todo lo que no pudo responder en lugar de una marca de visto: escanea tu app.
Dónde va un secreto de verdad
En una máquina de la que tus visitantes nunca descargan nada. En un proyecto de Supabase es una Edge Function, un pequeño trozo de código de servidor que Supabase ejecuta por ti; en una app de Next.js es una ruta de servidor; en una app de Replit es la mitad del servidor.
La forma es la misma en todas partes. Tu código del navegador le pide a tu función que haga el trabajo. La función guarda la clave, hace la llamada a OpenAI o Stripe y devuelve la respuesta. La clave se queda en la máquina y el visitante recibe un resultado. El artículo sobre OpenAI lo dibuja como un objeto que se mueve una caja a la derecha, y el cambio es solo eso.
Dos cosas te dicen que el builder hizo lo que pediste. La variable perdió su
prefijo, así que es OPENAI_API_KEY, y vive en los secretos de la propia
función, definidos en el panel de Supabase bajo Edge Functions, sin nada en el
.env de la app. Y el archivo que la lee está bajo supabase/functions/ o
app/api/, en algún sitio que la compilación nunca empaqueta, en vez de bajo
src/.
Un orden importa, y es fácil hacerlo al revés. Si una clave secreta ya se envió detrás de un prefijo, moverla no hace nada por las copias ya descargadas. Rótala primero en el proveedor, y después mueve el trabajo. Si rotar primero o cerrar primero la fuga depende de si la copia ya es pública, y una vez que estuvo en un bundle publicado, lo es.
En un proyecto de Replit la misma separación tiene nombre propio, Secrets, y lo que cubre la herramienta Secrets y lo que no es un artículo aparte.
Qué hacer ahora mismo
Qué hacer
- Busca en tu proyecto
VITE_,NEXT_PUBLIC_,EXPO_PUBLIC_yREACT_APP_. Cada coincidencia es un valor que tu compilación publica a propósito. Decide para cada uno si puede ser público. - Conserva la dirección y la clave publicable.
VITE_SUPABASE_URLyVITE_SUPABASE_PUBLISHABLE_KEY, o la claveanonen un proyecto antiguo, son el par para el que existe el prefijo. - 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ó.
- 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.
- Restringe una clave de Google por referrer en Google Cloud. Esa necesita un ajuste y conserva su valor.
- Después de la siguiente publicación, busca en la app en producción cada valor que moviste.
Cada publicación empaqueta una caja nueva. La siguiente función que pidas es otra oportunidad de que un valor reciba el prefijo, y nada entre el builder e internet lee el bundle de camino a la salida. Un escaneo del mes pasado leyó el bundle del mes pasado.
Reeve Monitor lee el bundle por ti. Vuelve a ejecutar las nueve comprobaciones cada hora en hasta tres apps, te avisa cuando un resultado cambia, vigila la disponibilidad cada 60 segundos y envía un informe mensual. Una clave que llega al bundle con la publicación del martes aparece en el reescaneo de esa hora, te acordaras de mirar o no. Cuesta $12 al mes a precio de lista, con siete días gratis antes de cobrarte; la página de precios a veces está por debajo de la cifra de aquí y nunca por encima.
Si prefieres recorrer esto como una lista, la lista de seguridad de 10 minutos cubre esto y las demás cosas que conviene desactivar en una app recién lanzada.
Preguntas frecuentes
¿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í.
¿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.
¿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.
¿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.
¿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.
¿.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.