Saltar al contenido

Fundamentos de seguridad

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.

Vlad Tkachenko12 min de lectura
Una hoja con un agujero en forma de llave, y detrás una caja sellada con un candado cerrado y la llave visible tras una ventanilla.

En resumen

  • Nada en un navegador sabe guardar un secreto, así que ocultar una clave API significa moverla a algo que sí sepa. Una Edge Function de Supabase es el servidor más pequeño que puedes tener.
  • La mudanza son cuatro pasos. Sustituye antes la clave que ya publicaste, porque la copia que hay en tu bundle antiguo sigue siendo legible después de que la quites.
  • Una function desplegada exige un token por defecto, y la clave pública de tu propio frontend cumple esa exigencia. Comprobar quién llama es un paso aparte, y es el que impide que un desconocido gaste tu cuota.

Todas las guías sobre una clave API filtrada acaban en el mismo sitio: muévela a un servidor. Y ahí se paran. Te quedas con una app que construiste en Lovable, Bolt o Cursor, ningún servidor a la vista, y una frase que da por hecho que ya sabes qué hacer a continuación.

Esto es lo que guía tras guía deja fuera. Mover la clave a una Edge Function de Supabase la saca de tu página, y por sí solo eso no impide que un desconocido la use. La propia documentación de Supabase dice por qué: la comprobación que una function desplegada ejecuta por defecto acepta también tu clave pública, y tu clave pública está en tu frontend, al alcance de cualquiera que quiera copiarla. Ocultar la clave es la mitad sencilla. De la otra mitad no escribe nadie: decidir a quién responde tu function nueva.

Piensa en la clave como la llave maestra de un almacén. Ahora mismo está pegada por dentro del escaparate, donde la lee cualquiera que se pare. Una Edge Function es una trastienda con ventanilla: la llave entra ahí, y los visitantes preguntan por la ventanilla en vez de pasar y servirse. Que eso sea una mejora depende de a quién se le abre la ventanilla.

¿Dónde pongo mi clave API si no es en el frontend?

En cualquier sitio que no sea el navegador. Una Edge Function es la versión más pequeña de eso.

Un navegador no sabe guardar un secreto. Todo lo que tu página necesita para funcionar lo descarga cada visitante, y un visitante puede leerlo todo: el código, las imágenes, los valores compilados dentro del código. Eso no es un fallo de tu herramienta. Es lo que es una página web. Qué claves API son seguras en tu frontend es la versión larga; la corta es que una clave sk_, una clave de OpenAI o una clave secreta de Supabase nunca iba a sobrevivir a un viaje al navegador.

Una Edge Function de Supabase es un trozo pequeño de código que se ejecuta en las máquinas de Supabase. Puede leer los secretos que hayas puesto en tu proyecto, responde en una dirección web propia, y tu app la llama por su nombre. Escribes un archivo, Supabase lo ejecuta, y no hay servidor que alquilar, parchear ni mantener despierto.

Arriba: la clave está en el archivo que descarga cada visitante, así que la tiene cada visitante. Abajo: el navegador le pregunta a la function, y la clave nunca sale de Supabase.

Sustituye la clave que ya publicaste, antes de mover nada

La clave que hoy está en tu frontend ya es pública, y lo sigue siendo después de que la saques del código.

Cada visitante que cargó tu sitio la descargó. Cada rastreador también, y los hay automáticos recorriendo toda la web en busca exactamente de estas cadenas. Tu bundle antiguo sigue además en tu historial de versiones y en cualquier caché que tenga una copia de tu página. Borrar una línea de un archivo que controlas no cambia nada sobre un valor que ya se ha repartido.

Así que el orden es: crear una clave nueva en el proveedor, guardarla un momento, y revocar la antigua en cuanto la function de abajo esté en marcha. Lee después tu página de facturación y tus registros de uso de todo el periodo en que la clave antigua estuvo fuera. Sustituirla detiene lo que viene; no tiene ningún efecto sobre lo que ya pasó. Si la clave era una clave secreta de Supabase, el orden en que trabajar tiene un matiz que conviene leer antes.

Los cuatro pasos

Guardar el secreto, escribir la function, desplegarla, y cambiar tu app para que llame a la function en vez de al proveedor.

PasoEn la línea de comandosEn el panel
1. Guardar el secretosupabase secrets set MY_API_KEY=…Edge Functions → Secrets, luego Key y Value
2. Escribir la functionsupabase functions new forward-requestEdge Functions → Deploy a new function → Via Editor
3. Desplegarlasupabase functions deployEl botón Deploy bajo el editor
4. Llamarla desde tu appsupabase.functions.invoke('…')Igual, en el código de tu app

Dentro de la function el secreto llega como variable de entorno, es decir, un valor con nombre que el código puede leer y nadie de fuera. La documentación de Supabase sobre secretos de Edge Functions tiene la referencia completa, y tres detalles de ahí ahorran una hora de confusión:

  • El nombre de un secreto no puede empezar por SUPABASE_. Ese prefijo está reservado para los valores que Supabase pone por ti, y lo rechazan tanto el panel como la API.
  • Un secreto nuevo se lee de inmediato. No hay que volver a desplegar la function después de cambiar uno.
  • Local y producción van por separado. La pila local lee supabase/functions/.env, que es un sitio distinto de los secretos de tu proyecto en marcha, así que pon el valor en ambos o la function funcionará en tu máquina y fallará una vez desplegada.

Borra después la variable antigua de tu frontend. Si se llamaba VITE_, NEXT_PUBLIC_ o EXPO_PUBLIC_, ese prefijo era la instrucción de compilar el valor dentro del bundle, y el prefijo es toda la historia de cómo llegó ahí.

¿Exigir un token significa que solo llaman mis usuarios?

No, y este es el punto del artículo que merece dos lecturas.

Supabase activa por defecto una comprobación llamada verify_jwt. Mira la cabecera Authorization antes de que corra tu código y rechaza la petición si ahí no hay nada válido. Suena a puerta que solo abren tus usuarios. No lo es, porque Supabase documenta además que la misma comprobación acepta una clave pública o secreta en cualquiera de las dos cabeceras, por compatibilidad con proyectos antiguos, y añade con claridad que la comprobación por sí sola no autentica a quien llama enviando únicamente una clave API.

Tu clave pública está en tu frontend. Eso es correcto y está pensado así. También significa que quien abra tu sitio, lea la clave y se la envíe a tu function nueva pasa la comprobación de la plataforma y entra en tu código.

En el almacén, verify_jwt es un portero que comprueba que llevas una acreditación de visitante. La lleva cada visitante, porque las repartes tú en la puerta. Quien está en la ventanilla hace otro trabajo, y es quien pregunta qué visitante eres.

Los dos pasan la comprobación de la plataforma. Solo el segundo pasa una comprobación de quién llama, y esa segunda es la que tienes que añadir tú.

Cerrar la function, para no haber montado un proxy abierto

Decide a qué llamantes acepta tu function, y escribe esa decisión dentro de la function.

Supabase trae un envoltorio para esto, así que es una línea y no un proyecto. Pon auth: 'user' y la function acepta el token de un usuario con sesión iniciada y le da a tu código un cliente de base de datos ya limitado a las reglas de Row Level Security de ese usuario:

import { withSupabase } from 'npm:@supabase/server@1'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    // ctx.userClaims dice quién llama. Rechaza aquí lo que no quieras,
    // y luego llama al proveedor con el secreto del entorno.
    return Response.json({ ok: true })
  }),
}

La página Securing Edge Functions enumera los demás modos, y dos de ellos importan en una app corriente. Una function a la que llama otra máquina y no un navegador usa auth: 'secret', con la clave secreta enviada en la cabecera apikey. Una function que recibe webhooks de Stripe o de GitHub no puede usar ninguno de los dos, porque esos proveedores no tienen ningún token tuyo; pone verify_jwt = false y comprueba la firma del proveedor dentro del handler.

Elijas lo que elijas, decide a conciencia qué pasa cuando aparece un llamante con el que no contabas. El ejemplo que da Supabase de una function que puede aceptar a cualquiera es una comprobación de estado, y una comprobación de estado no cuesta nada cuando la llama un desconocido. Una function que reenvía una llamada de API de pago te factura cada una.

¿Puedo usar service_role en una Edge Function?

Sí. Una function es el único sitio donde una clave secreta de Supabase tiene cabida.

Tampoco tienes que pegarla. Supabase pone las claves del proyecto en el entorno de la function, así que el código las lee de ahí y no de un secreto que hayas puesto tú. Los proyectos nuevos reciben SUPABASE_SECRET_KEYS y SUPABASE_PUBLISHABLE_KEYS, cada uno un pequeño diccionario de claves con nombre; los proyectos más antiguos reciben SUPABASE_SERVICE_ROLE_KEY y SUPABASE_ANON_KEY con sus nombres de siempre. Qué cambió cuando Supabase renombró sus claves te dice qué par tienes.

Usa la clave secreta para el trabajo que de verdad tiene que ver cada fila, como escribir un registro de auditoría que el usuario no pueda editar. Para todo lo que un usuario lee sobre sí mismo, usa su propio token y deja que filtren tus reglas de Row Level Security, que para eso están.

Comprobarlo desde el navegador, la única prueba real

Carga tu sitio en marcha, abre la pestaña de red, y mira qué envía de verdad tu navegador.

Las dos cosas a comprobar:

  • Ninguna petición lleva la clave. Recorre la parte de tu app que antes llamaba al proveedor. La petición saliente debe ir a …supabase.co/functions/v1/tu-function, y la única credencial que haya dentro debe ser tu clave pública o el token de tu usuario.
  • La clave no está en el código descargado. Usa la búsqueda de tu navegador en todos los archivos cargados y pega la primera docena de caracteres de la clave antigua. Ningún resultado es la respuesta que quieres.

Solo una reconstrucción y un nuevo despliegue sacan un valor del bundle. Una clave que sigue apareciendo en la búsqueda suele significar que el frontend salió antes de que saliera la variable.

Nuestro escaneo gratuito hace la segunda comprobación desde fuera y nombra lo que puede leer en tu bundle en marcha, incluida qué clave de Supabase has publicado. Tarda unos 20 segundos y no necesita cuenta: escanea tu app.

Cómo hacerlo desde Lovable, Bolt o Replit

Usa el panel de Supabase. Ahí no hay ningún paso de terminal.

Abre tu proyecto, elige Edge Functions en la barra lateral y luego Deploy a new function → Via Editor. La guía rápida del panel de Supabase lo recorre con capturas, y entre las plantillas que ofrece hay una para reenviar a un proveedor de IA, que es exactamente la forma de este trabajo. El despliegue tarda entre diez y treinta segundos, y la function queda en marcha en una dirección propia. Tu secreto va en la página Edge Function Secrets, en la misma sección.

Una advertencia que conviene conocer antes de confiarte: Supabase escribe que el editor del panel no tiene control de versiones, ni versiones, ni vuelta atrás, y lo recomienda para trabajo rápido y no para código que vaya a quedarse. Pulsar Deploy sobrescribe lo que había. Si la function acaba haciendo algo que te importa, descárgala desde esa página y guarda el archivo.

Hay además una versión de esta pregunta con forma de Replit, porque Replit tiene su propio almacén de secretos y tu app ejecuta allí su propio servidor. Qué hacen de verdad los Replit Secrets es esa versión.

Cuándo una Edge Function es la respuesta equivocada

Dos casos, y en los dos la mudanza te cuesta trabajo y no aporta nada.

La clave era pública por diseño. Una clave pk_live_ de Stripe, una clave pública de Supabase o una clave anon, un token público de Mapbox: están hechas para vivir en un navegador, y la protección está en otro sitio. Poner una detrás de una function añade un rodeo y no quita ningún riesgo. Cuáles son esas claves es la lista.

La clave se puede restringir a tu propio sitio. Las claves de navegador de Google son el ejemplo habitual: en la consola de Google puedes limitar una a tus dominios, y esa es justo la solución que Google prevé para este caso. La clave sigue siendo legible y deja de servirle a quien la copie.

Todo lo demás va en un servidor. Una clave de OpenAI o de Anthropic no tiene variante pública, y por eso una clave de proveedor de IA en tu frontend no tiene ningún ajuste que la salve, y ninguna minificación esconde una clave secreta de Stripe de quien lea tu página.

Qué hacer ahora mismo

Qué hacer

  • Sustituye la clave primero. Crea una nueva en el proveedor, ponla en el secreto de la function, y revoca la antigua en cuanto la function esté en marcha.
  • Guarda el secreto en tu proyecto de Supabase, no en tu repositorio y no en una variable VITE_. Ponlo por separado para desarrollo local y para producción.
  • Despliega una function que use el secreto, y cambia tu app para que llame a la function por su nombre en vez de al proveedor.
  • Añade una comprobación de quién llama. verify_jwt por sí solo acepta la clave pública de tu propio frontend, así que no deja fuera a un desconocido.
  • Borra la variable antigua, reconstruye, y confirma en la pestaña de red de tu navegador que nada de lo que sale lleva la clave.
  • Lee la facturación y el uso de todo el periodo en que la clave antigua fue pública. Sustituirla detiene el próximo cargo y no el último.

Si prefieres repasar tu app entera en vez de una sola clave, la checklist de seguridad en 10 minutos cubre esto junto a las otras cosas que conviene cerrar en una app recién lanzada.

Preguntas frecuentes

¿Dónde pongo mi clave API si no es en el frontend?

En cualquier sitio que no sea el navegador. Una Edge Function de Supabase es la opción más pequeña: guardas la clave como secreto en tu proyecto, escribes un archivo pequeño que la usa, y tu app llama a esa function por su nombre en lugar de llamar al proveedor directamente. Una ruta serverless en tu hosting o un servidor tuyo funcionan igual. Lo que tienen en común es que la clave se lee donde tu visitante no la ve.

¿Qué es una Edge Function de Supabase?

Un trozo pequeño de código que se ejecuta en las máquinas de Supabase y no en el navegador de tu visitante. Responde en una dirección web propia, puede leer los secretos que hayas puesto en tu proyecto, y tu app lo llama con supabase.functions.invoke. Escribes un archivo, Supabase lo ejecuta, y no hay ningún servidor que alquilar ni mantener.

¿Tengo que sustituir mi clave después de moverla?

Sí, y primero. La clave que estaba en tu frontend la ha descargado cada visitante y cada rastreador que leyó tu sitio, y sacarla del código no cambia nada sobre esas copias. Crea una clave nueva en el proveedor, ponla en el secreto de tu Edge Function y revoca la antigua. Después revisa la facturación y el uso de todo el periodo en que la clave antigua estuvo activa.

¿Puede cualquiera llamar a mi Edge Function?

Por defecto una function rechaza una petición sin ningún token, pero esa comprobación es más débil de lo que suena. Supabase documenta que la comprobación de la plataforma acepta también tu clave pública o secreta, y tu clave pública está en tu frontend, donde cualquiera puede leerla. Así que la comprobación detiene una petición vacía y no una decidida. Para aceptar solo a tus usuarios con sesión iniciada, verifica a quien llama dentro de la function.

¿Puedo usar service_role en una Edge Function?

Sí. Una Edge Function es el único sitio donde una clave secreta de Supabase tiene cabida, y Supabase pone las claves del proyecto en el entorno de la function por ti, así que nunca pegas ninguna. Úsala para el trabajo que tiene que ver cada fila, y usa el token de quien llama para todo lo que deba respetar tus reglas de Row Level Security.

¿Puedo hacer esto sin terminal?

Sí. El panel de Supabase tiene una sección Edge Functions donde escribes una function en el navegador, pulsas Deploy y pones tu secreto en la página Edge Function Secrets. Supabase advierte de que el editor del panel no guarda historial de versiones, así que lo recomienda para trabajo rápido y deja la línea de comandos para lo que pienses conservar.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

Todos los artículos

¿No sabes cómo está tu propia app?

Haz un escaneo gratuito y obtén una nota clara de la A a la F en unos 20 segundos. Sin cuenta y sin tarjeta.

Analizar mi app gratis

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.