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

En resumen
- "Auth session missing!" (AuthSessionMissingError, estado 400) sale de supabase-js, dentro de tu aplicación. La biblioteca buscó una sesión iniciada, no encontró ninguna y nunca envió la petición a Supabase.
- En una página donde nadie ha iniciado sesión es la respuesta correcta, y tu código solo tiene que tratarla. En una página para restablecer la contraseña significa que el enlace no inició la sesión de nadie aquí. En una Edge Function o en un servidor, la sesión del visitante nunca llegó.
- Deja getUser() donde está. Cambiarlo por getSession() en un servidor hace desaparecer el error creyéndose lo que diga la cookie.
Tu aplicación de Lovable o Bolt inicia la sesión de la gente con Supabase. Una mañana abres la consola del navegador y encuentras esto en rojo, en una página donde nada parece ir mal:
AuthSessionMissingError: Auth session missing!
O una clienta te escribe para decirte que la página "Elige una contraseña nueva" la rechaza con esas mismas palabras, cada vez.
Busca "auth session missing supabase" y encontrarás hilos llenos de arreglos:
cambiar getUser() por getSession(), bajar de versión la biblioteca, añadir
una espera, reiniciar el proyecto. Dan por hecho que algo se rompió entre tu
aplicación y Supabase. No se hizo ninguna petición. El error se escribe
dentro de tu aplicación, lo escribe la biblioteca de Supabase, antes de que nada
salga del navegador.
¿Qué significa "Auth session missing!" en Supabase?
A supabase-js le preguntaron quién había iniciado sesión, buscó una sesión, no encontró ninguna y respondió por su cuenta.
Una sesión es lo que Supabase Auth le da a una persona cuando inicia sesión: un par de tokens que dicen quién es, guardados por el navegador para que la siguiente carga de página la siga reconociendo. supabase-js es la biblioteca con la que tu aplicación habla con Supabase, y cada llamada que necesita a una persona empieza por sacar esa sesión.
El mensaje está escrito en el propio supabase-js, en el archivo errors.ts de la
biblioteca de autenticación: un AuthSessionMissingError con el texto
Auth session missing! y el estado 400. Ese 400 es una etiqueta que se pone la
propia biblioteca. Ningún servidor respondió con él, y los registros de Supabase
no mostrarán nada, porque getUser() busca una sesión antes de hacer cualquier
otra cosa. Un comentario en su código lo dice: devuelve un error si no hay ni
token de acceso ni una cabecera Authorization propia.
Imagina una recepción que sabe decirte qué visitante es cada uno. A cada persona que inicia sesión le dan una credencial en la puerta, y supabase-js guarda esa credencial en el bolsillo del navegador. Cuando tu código pregunta "¿quién es este?", supabase-js mira primero en el bolsillo. Si hay una credencial, la lleva a recepción y vuelve con un nombre. Si el bolsillo está vacío, ni siquiera va a recepción. Vuelve en el acto y te dice que no hay credencial.
Se devuelve, no se lanza
getUser() devuelve el error junto a un usuario vacío, y tu código decide qué
pasa después.
La llamada es así, y sin sesión user queda en null y error contiene el
AuthSessionMissingError:
const { data: { user }, error } = await supabase.auth.getUser()
No se lanza nada, así que un try … catch alrededor de la llamada no atrapa
nada. De ahí viene el daño visible, con una de dos formas:
- El código muestra cualquier
errorque recibe. Un visitante que aún no ha iniciado sesión ve "Auth session missing!" en un aviso o en un recuadro rojo, en una página que tenía derecho a ver. - El código usa el usuario sin comprobarlo. Lee
user.idy la página se rompe conCannot read properties of null, que parece otro fallo distinto.
Las dos son el mismo error: tratar "nadie ha iniciado sesión" como una avería. Comprueba primero el usuario, y dale a un visitante sin sesión la pantalla pensada para él:
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
// nadie ha iniciado sesión: mostrar la pantalla de acceso o la página pública
return
}
Si quieres distinguir esto de una avería de verdad, la biblioteca le pone nombre:
error.name es 'AuthSessionMissingError'.
Dónde se pierde la sesión
En tres sitios, y solo el primero es inofensivo.
1. Nadie ha iniciado sesión en este navegador. Tu aplicación llama a
getUser() en una página que cualquiera puede abrir, quizá para decidir si
muestra "Iniciar sesión" o "Mi cuenta". Para un visitante, el bolsillo vacío es
la respuesta verdadera. Aquí es donde la mayoría se encuentra el mensaje, y el
arreglo de código de arriba es todo lo que necesita.
2. Una página para restablecer la contraseña o de enlace mágico. Poner una
contraseña nueva llama a updateUser({ password }), y la
guía de contraseñas de Supabase
es clara con el orden: es una vez que hay sesión cuando se recoge la contraseña
nueva y se llama a updateUser. En esa página, la sesión tiene que venir del
enlace del correo, que inicia la sesión de la persona al abrirse. Cuando no lo
hace, el bolsillo está vacío y updateUser() responde "Auth session missing!".
Las razones habituales:
- El enlace ya se había usado, o había caducado, antes de abrirlo. Por qué Supabase dice que el enlace del correo no es válido o ha caducado repasa cada una.
- Se abrió en otro navegador o aplicación distinto del que lo pidió. Cuando una aplicación usa el inicio de sesión PKCE de Supabase, parte del intercambio se queda en el navegador que lo empezó, y según la documentación de Supabase el intercambio del código tiene que iniciarse en el mismo navegador y dispositivo en que empezó el proceso.
- Mandó a la persona a otro sitio, como
localhost, así que la sesión llegó a una dirección que no es tu aplicación. Por qué Supabase redirige a localhost cubre ese caso.
3. Tu servidor o una Edge Function nunca recibió la sesión. La credencial
está en el navegador de tu visitante. El código que corre en otra parte, una Edge
Function de Supabase o la mitad de servidor de una aplicación Next.js hecha con
v0 o Cursor, crea su propio cliente de Supabase, y un cliente nuevo empieza con
el bolsillo vacío. Mientras no le pases la sesión del visitante, getUser() no
encuentra nada allí y responde "Auth session missing!" sin preguntar a Supabase.
En una Edge Function la sesión viaja en la cabecera Authorization de la
petición, y supabase-js solo la usa si se la entregas al crear el cliente. En una
aplicación Next.js viaja en cookies, que la herramienta de servidor de Supabase
solo lee si le das las de la petición. Si una Edge Function devuelve ese fallo
como error, tu aplicación muestra
"Edge Function returned a non-2xx status code",
con este mensaje esperando en el registro de la función.
Cómo saber cuál es tu caso
Mira dónde aparece el mensaje, y luego qué envió de verdad el navegador.
En la consola del navegador, en una página que cualquiera puede abrir. Caso
- Abre las herramientas de desarrollo de tu navegador (F12), elige
Application, luego Local Storage y la dirección de tu aplicación.
supabase-js guarda la sesión bajo una clave formada por
sb-, el identificador de tu proyecto y-auth-token. Si no hay ninguna entrada así, este navegador no tiene sesión, y el mensaje dice la verdad. Una aplicación Next.js que usa las herramientas de servidor de Supabase guarda la sesión en cookies, así que busca el mismo nombre en Cookies.
En una página para restablecer la contraseña o de enlace mágico. Caso 2. Mira la dirección en la que se abrió el enlace. Si no es tu aplicación, o es un enlace que tú o un escáner de correo ya habíais abierto, la sesión no tuvo ninguna oportunidad de llegar.
En el registro de una Edge Function, o de tu servidor. Caso 3. Abre la
pestaña Network, repite la acción y haz clic en la petición a tu función. Si
su cabecera Authorization lleva una cadena larga que empieza por eyJ, el
navegador envió el token de una persona, y tu función no se lo pasó a su cliente.
Lo que no encontrarás por ninguna vía es una petición a /auth/v1/user que
volviera con este mensaje. Si ves una que falló, es otro error. Una respuesta que
dice invalid claim: missing sub claim significa que la clave de tu proyecto se
envió donde debía ir una persona, y ese artículo
es su gemelo.
El arreglo para cada caso
Dale a cada uno la sesión que le falta, o una respuesta que no la necesite.
Caso 1, el visitante sin sesión: comprueba user antes de usarlo, como en el
código de arriba, y muestra la pantalla pensada para alguien que no ha iniciado
sesión.
Caso 2, la página para restablecer la contraseña: espera a que el enlace
haya iniciado la sesión de la persona antes de mostrar el formulario. Supabase
emite un evento PASSWORD_RECOVERY cuando alguien llega desde un enlace de
recuperación, y su referencia muestra la página esperándolo:
supabase.auth.onAuthStateChange((event) => {
if (event === 'PASSWORD_RECOVERY') {
// el enlace inició su sesión: ahora mostrar el formulario de contraseña nueva
}
})
Si el evento no llega nunca, el enlace no funcionó. Dilo y ofrece enviar uno nuevo, en lugar de mostrar un formulario que va a fallar con seguridad.
Caso 3, la función o el servidor: pásale la sesión del visitante al cliente que creas. En una Edge Function, créalo con la cabecera que llega:
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } },
)
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return new Response(JSON.stringify({ error: 'Sign in first' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
El envoltorio más reciente de Supabase, withSupabase({ auth: 'user' }), hace lo
mismo antes de que se ejecute tu manejador, y el
artículo sobre Edge Functions muestra
cómo montarlo. Mantén el 401 para quien llame sin sesión, para que una petición
desde una página sin sesión reciba un rechazo claro. Con la cabecera pasada,
getUser() puede devolver ahí invalid claim: missing sub claim para quien
llame sin sesión: el mismo visitante visto desde el otro lado, y el 401 cubre los
dos.
En una aplicación Next.js, crea el cliente de servidor con la herramienta
@supabase/ssr de Supabase y las cookies de la petición, como muestra su
configuración de servidor, y llama a getUser() en ese cliente.
Dos arreglos que lo empeoran
Los dos se sugieren para este mensaje, y cada uno para el error quitando la comprobación que hay detrás.
Cambiar getUser() por getSession(). En el navegador no cambia nada que
importe: el almacenamiento sigue vacío, así que no obtienes ni sesión ni usuario,
solo que sin el mensaje. En un servidor es peligroso. La guía de servidor de
Supabase dice que nunca hay que fiarse de supabase.auth.getSession() en código
de servidor, porque lee la sesión de la cookie sin volver a validarla. Una cookie
es algo que te manda el visitante, y cualquiera puede mandarte la cookie que
quiera. getUser() es el método del que supabase-js documenta que hace una
petición al servidor de Supabase Auth, así que su resultado es auténtico.
Mantenlo en todos los sitios donde la respuesta decide qué puede hacer alguien.
Poner la clave secreta en el cliente para que la llamada deje de fallar. Un
cliente creado con la clave service_role o sb_secret_ no necesita sesión para
leer y escribir, porque se salta por completo las reglas de tu base de datos. La
función vuelve a funcionar, para cualquiera que la llame, con sesión o sin ella.
Si un builder hace ese cambio en código que va al navegador, la clave viaja con
él a cada visitante. Si ya ha pasado, rótala primero.
Eso se puede comprobar desde fuera. Nuestro escaneo gratuito lee el JavaScript de tu aplicación publicada como lo haría un desconocido y nombra cada clave que encuentra, incluido si es una de las que deben estar ahí. Tarda unos 20 segundos y no pide cuenta: escanea tu app.
Que la comprobación siga ahí tras el próximo cambio
La comprobación de quién ha iniciado sesión es la parte de una página que más fácil desaparece cuando le pides a tu builder que haga desaparecer un error, y los dos arreglos rápidos de arriba la quitan.
Reeve Monitor vuelve a escanear tu app publicada cada hora:
- las nueve comprobaciones cada hora, en hasta tres apps
- un correo el día en que un nuevo escaneo califica tu app peor que el anterior; una clave secreta en tu JavaScript limita por sí sola la nota a D
- si la app está disponible, cada 60 segundos
- un informe mensual de lo que vio
Un servidor que se cree cualquier cookie, o una función que funciona con la clave secreta, deja que un desconocido cambie las filas de tus usuarios. Reeve Care guarda una copia de tu base de datos de Supabase de antes de que eso pase.
- una copia cifrada según un calendario, guardada en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
- cada copia verificada antes de contar, contando las filas de cada tabla
- una restauración en un clic que guarda lo que hay ahora antes de reemplazarlo
- también tus archivos subidos, en cuanto conectes una credencial de Storage
- todo lo que hace Monitor
Qué hacer ahora
Qué hacer
- Lee "Auth session missing!" como "aquí nadie ha iniciado sesión". La petición nunca salió de tu aplicación.
- Comprueba si
useresnullantes de usarlo, y dale a un visitante sin sesión la pantalla pensada para él en lugar del error. - En una página para restablecer la contraseña, espera al evento
PASSWORD_RECOVERYantes de mostrar el formulario, y ofrece un enlace nuevo si nunca llega. - En una Edge Function o en un servidor, pásale la cabecera
Authorizationo las cookies del visitante al cliente que creas, y responde 401 si sigue sin haber usuario. - Mantén
getUser()en el servidor. AllígetSession()se cree cualquier cookie que le manden.
Si tus sesiones existen y luego desaparecen, de modo que gente que había iniciado sesión se ve fuera de repente, es otro problema con sus propias causas, y por qué se cierra la sesión de los usuarios de Supabase lo cubre. La lista de seguridad de 10 minutos cubre lo que una aplicación recién lanzada suele dejar abierto además.
Preguntas frecuentes
¿Qué significa "Auth session missing!" en Supabase?
Significa que a supabase-js, la biblioteca de Supabase dentro de tu aplicación, le pidieron el usuario con sesión iniciada y no tenía ninguna sesión con la que preguntar. Una sesión es lo que Supabase Auth le da a una persona cuando inicia sesión, y la biblioteca la guarda en el navegador. Si no hay ninguna, no contacta con Supabase en absoluto. Devuelve AuthSessionMissingError con el estado 400 y un usuario null.
¿Por qué veo "Auth session missing!" cuando nadie ha iniciado sesión?
Porque esa es la respuesta para un visitante que no ha iniciado sesión. Tu aplicación llamó a getUser() en una página que cualquiera puede abrir, y supabase-js informó de que nadie había iniciado sesión. No hay nada roto. Tu código solo tiene que leer el usuario como null y mostrar la pantalla de acceso, en lugar de mostrar el error.
¿Por qué mi página para restablecer la contraseña dice "Auth session missing!"?
Cambiar una contraseña con updateUser() necesita una sesión, y en esa página la sesión tiene que venir del enlace del correo. Si el enlace ya se había usado o había caducado, se abrió en otro navegador distinto del que lo pidió, o mandó a la persona a otra dirección, no llegó ninguna sesión y updateUser() no tiene a quién cambiar. Espera al evento PASSWORD_RECOVERY antes de mostrar el formulario, y ofrece un enlace nuevo si nunca llega.
¿Es lo mismo que "invalid claim: missing sub claim"?
No. "Auth session missing!" se decide dentro de tu aplicación, antes de que salga ninguna petición. "invalid claim: missing sub claim" vuelve de Supabase Auth, que recibió una petición y encontró la clave de tu proyecto donde debía estar el token de una persona. Lo primero significa que no había nada que enviar. Lo segundo, que se envió lo que no era.
¿Debo usar getSession() en lugar de getUser()?
No para quitar este error. En el navegador, getSession() encuentra el mismo almacenamiento vacío y no devuelve ninguna sesión, así que nada cambia salvo que el mensaje desaparece. En un servidor es peor: supabase-js documenta que getSession() lee la sesión directamente del almacenamiento, por ejemplo de las cookies de la petición, que pueden no ser auténticas, y recomienda getUser() en su lugar porque lo comprueba con Supabase Auth.