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

En resumen
- Un error Supabase invalid JWT llega como PGRST301 con HTTP 401, y significa que la firma del token que iba en tu petición no se pudo verificar. No se buscó nada en una lista para luego rechazarlo.
- Las claves publishable y secret son cadenas cortas sin firma, así que en un proyecto que ya se pasó a ellas este error habla de tu usuario con sesión iniciada y nunca de la clave de API.
- PostgREST 13 cambió una frase vaga por una lista corta de frases concretas. Lee el mensaje que te llegó de verdad: cada uno descarta una causa distinta.
Ayer tu aplicación funcionaba. Esta mañana cada petición vuelve con un 401, en tu código no ha cambiado nada, y la respuesta habla de un JWT, algo que tú no has escrito nunca en ningún sitio.
{
"code": "PGRST301",
"message": "JWT cryptographic operation failed"
}
Un error Supabase invalid JWT es el que aparece después de hacer lo correcto. Rotaste una clave porque una guía te lo dijo, o pasaste tu proyecto a las claves nuevas, y la aplicación se quedó callada.
Y aquí es donde guía tras guía se equivoca: la clave de la que habla este
mensaje a menudo no es la que acabas de cambiar. Busca el error y te mandarán a
revisar tu clave anon. En un proyecto que ya se pasó a las claves publishable y
secret, la clave de API no es un token firmado en absoluto, así que lo que no
pasó la verificación era otra cosa de la petición.
¿Qué significa un error Supabase invalid JWT?
Algo de tu petición era un token firmado, y la firma no coincidió.
Una contraseña es una palabra que alguien busca en una lista. Un JWT no es eso. Es una carta cerrada con lacre: la persona de la puerta no te busca en ningún sitio, sostiene un sello y comprueba si el lacre de tu carta se hizo con él. Si es así, pasas, y nadie lee ninguna lista.
Una carta cerrada con el sello equivocado se rechaza aunque cada palabra de dentro sea correcta. Y en cuanto la puerta cambia de sello, ninguna carta que ya esté en camino se abre, sin que nadie haya tocado las cartas.
Tu petición es la carta. Tu proyecto de Supabase sostiene el sello. Un 401 aquí significa que los dos no coincidieron.
El mensaje cambió en PostgREST 13 y el código no
Comprueba PGRST301. La frase que lo acompaña depende de la versión de PostgREST
que ejecute tu proyecto, y las más nuevas te dan una lista corta de ellas.
La referencia de errores de PostgREST describe PGRST301 como un JWT recibido
que no se pudo decodificar o que no es válido, con HTTP 401, tanto en la versión
13 como en la 14. Esa frase es la descripción de la tabla de documentación. No es
lo que aterriza en tu campo message. Leyendo Error.hs en la rama main de
PostgREST, ese campo lleva una de estas:
| Lo que dice el mensaje | Lo que te dice |
|---|---|
Empty JWT is sent in Authorization header | La cabecera sí se envió y venía vacía. |
Expected 3 parts in JWT; got 1 | Lo que llegó no es un JWT. Un JWT son tres bloques separados por puntos. |
No suitable key or wrong key type | El servidor no tiene ninguna clave capaz de verificar un token de ese tipo. |
Wrong or unsupported encoding algorithm | El token nombra un algoritmo de firma que el servidor no usa. |
JWT cryptographic operation failed | El token está bien formado y la firma no coincide. |
Unsupported token type | El token tiene una forma que PostgREST no lee. |
JWT cryptographic operation failed es el que significa clave caducada. El token
se construyó bien y lo firmó algo que el proyecto ya no tiene, que es
exactamente lo que deja atrás una rotación.
Los proyectos antiguos ven algo muy distinto, y de ahí viene la cadena que
aparece por todas partes en los resultados de búsqueda. Antes de la versión 13,
PostgREST imprimía lo que había dicho su biblioteca de JWT, tal cual. De ahí sale
JWSError JWSInvalidSignature: es un valor de biblioteca impreso en pantalla, no
una frase que alguien escribiera para ti. El changelog de PostgREST de la 13.0.0
describe el cambio como mensajes de error diagnósticos en lugar de internos
expuestos.
Vale la pena conocer un código vecino mientras estás aquí. PGRST303, también
401, es lo que recibes cuando la firma se verificó y el contenido del token fue
rechazado: caducado, todavía no válido, o con la audiencia equivocada. Si ese es
tu código, deja de mirar las claves y ve a mirar el reloj y la sesión.
¿Qué clave es: la de API o el token de tu usuario?
Depende de si tu proyecto todavía usa anon y service_role.
La documentación de Supabase sobre claves de API es explícita en la diferencia.
Las claves antiguas anon y service_role son JWT de larga duración, y por eso
empiezan por eyJ. Las claves publishable y secret más nuevas son cadenas cortas
y no son JWT en absoluto.
Ese único hecho parte el error en dos:
- Tu proyecto sigue usando
anonyservice_role. La clave de API de tu petición es en sí misma un token firmado, así que una clave de otro proyecto, una clave de antes de la rotación o un pegado truncado fallan la verificación de firma igual que lo haría una sesión mala. Cualquiera de los dos tokens de la petición puede ser el que falló. - Tu proyecto ya usa
sb_publishable_ysb_secret_. La clave de API ha dejado de ser un token, así que no hay ninguna firma en ella que algo pueda rechazar. Lo que queda por verificar es el token de acceso de tu usuario con sesión iniciada, que Supabase Auth emite aparte de la clave de API.
En la práctica, la primera pregunta no es qué clave está mal. Es si había alguien con la sesión iniciada cuando falló la petición. Si una página sin sesión funciona y una con sesión no, estás mirando sesiones. Si falla todo, incluida una página que no pide nada personal, estás mirando la clave de API, y tu proyecto es de los antiguos.
Si son sesiones y tu log de Auth dice además Invalid Refresh Token, a tus
usuarios se les cierra la sesión porque
terminó la sesión detrás de su token.
Si la respuesta vino de Supabase Auth y dice invalid claim: missing sub claim,
el token pasó la comprobación de su firma y no nombraba a nadie. Es una clave
enviada donde va el token de una persona, y
ese mensaje tiene su propio artículo.
Invalid JWT no es "No API key found in request"
Son dos mensajes distintos con dos causas distintas, y las soluciones no se parecen en nada.
Un invalid JWT significa que llegó un token y la firma no coincidió. "No API key
found in request" significa que no llegó
ninguna cabecera apikey, así que la petición se rechazó en la pasarela antes de
preguntar nada a tu base de datos.
La propia respuesta aclara cuál tienes. Una clave que falta la rechaza la
pasarela que hay delante de tu proyecto, así que el cuerpo lleva un message y
un hint y ningún code. Un invalid JWT llegó lo bastante lejos como para que
lo leyera PostgREST, y por eso vuelve con PGRST301 escrito.
Dónde sigue estando la clave antigua, por orden
Cuatro sitios, y conviene mirarlos en este orden porque es más o menos la frecuencia con la que cada uno es la respuesta.
- Los ajustes de proyecto de tu herramienta. Lovable, Bolt, v0 y Replit guardan cada uno su propio juego de variables de entorno, y si construiste con Cursor o Claude Code ese mismo juego está donde despliegues. Es la copia que casi todo el mundo actualiza, y la única que casi todo el mundo actualiza.
- Una clave escrita directamente en el código. Viaja dentro del JavaScript que descargan tus visitantes, así que cambiarla en el panel no cambia nada hasta que se edite el archivo y se reconstruya el sitio. Dónde está esa página y qué hace cada valor, si no la has abierto.
- Los secretos de Edge Function. Una función guarda su propio juego, separado del de tu frontend, así que sigue usando la clave antigua cuando todo lo demás ya ha cambiado. Supabase también advierte de que una función con el ajuste Verify JWT activado puede romperse al rotar una clave de firma.
- Un despliegue que nadie ha reconstruido. La clave quedó incrustada en la última construcción, así que la copia en producción la sigue enviando hasta que algo dispare una nueva. Tu panel enseña la clave nueva todo el rato, y eso es lo que hace difícil ver este caso: el sitio donde vas a comprobarlo es el único que ya está bien.
Qué clave envía de verdad tu aplicación en producción
Abre el sitio publicado en lugar de la vista previa y lee qué envía la página.
Una clave antigua de Supabase es una cadena larga que empieza por eyJ. Las
nuevas se anuncian en sus primeros caracteres: sb_publishable_ va en tu
aplicación y sb_secret_ nunca. Si encuentras la segunda en una página que puede
cargar un visitante, deja este artículo y
rótala antes de hacer nada más. Cuál de
las dos tienes, y
qué cambió cuando Supabase les puso nombres
nuevos, es lo que conviene resolver antes de
empezar a sustituir valores.
Para ver qué emite tu proyecto ahora mismo: todas las claves viven ya en un solo sitio del panel, en Settings y luego API Keys, las antiguas incluidas.
Si prefieres no leer un bundle minificado, nuestro escaneo gratuito lee tu aplicación en producción como lo haría un desconocido y te dice qué claves se ven desde fuera. Tarda unos 20 segundos y no necesita cuenta: escanea tu aplicación.
Rotar una clave de firma sin cerrar la sesión de todos
Cuál de los dos caminos de rotación tomes decide si tus usuarios lo notan.
La documentación de Supabase sobre claves de firma los pone uno al lado del otro.
Rotar el secreto JWT antiguo cierra la sesión de todos los usuarios activos
de inmediato, y anon y service_role hay que rotarlas a la vez. Las JWT
signing keys más nuevas funcionan de otra manera: una clave nueva empieza como
standby, al rotar pasa a ser la actual, y la que sustituye pasa a ser la anterior.
Mientras eso sea así, se siguen aceptando las firmas de las dos claves, y las
cartas lacradas la semana pasada todavía se abren. Revocar la clave antigua es el
paso aparte que pone fin a eso.
Dos avisos de esa misma documentación son los que pillan a la gente:
- Un componente que verifica los tokens por su cuenta contra el secreto JWT antiguo, con una biblioteca como jose o jsonwebtoken, se rompe al rotar. Sostiene el sello viejo y nadie le ha dicho que lo cambie.
- Una Edge Function con Verify JWT activado puede romperse por lo mismo.
Crear claves publishable y secret tampoco revoca tus claves antiguas. Siguen funcionando hasta que las desactivas, algo que la documentación señala como un paso aparte. Así que una aplicación que solo ha movido la mitad de sus copias sigue funcionando durante semanas, y el día en que alguien desactiva el par antiguo es el día en que la otra mitad empieza a responder 401.
Vigilar una aplicación que ha empezado a responder 401
Una aplicación que devuelve 401 a cada petición sigue en pie, sigue sirviendo una página y sigue respondiendo a cualquier comprobación de salud. En tu panel no se pone nada en rojo. Te enteras cuando alguien te escribe.
Reeve Monitor se entera primero:
- las nueve comprobaciones cada hora, en hasta tres aplicaciones
- un correo el día en que un nuevo escaneo puntúa tu aplicación peor que el anterior
- si la aplicación responde, cada 60 segundos
- un informe mensual de lo que vio
Una clave que tuviste que rotar con prisa suele ser una clave que estaba donde no debía, y desde aquí no puedes ver qué se leyó o se cambió con ella mientras estuvo fuera. Reeve Care guarda una copia de tu base de datos de Supabase anterior a eso.
- 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 con un clic que guarda lo que hay ahora antes de reemplazarlo
- también tus archivos subidos, en cuanto conectes un acceso de Storage
- todo lo que hace Monitor
Qué hacer hoy
Qué hacer
- Lee el campo
code, no la frase.PGRST301es una firma que no coincidió,PGRST303es un token cuyo contenido fue rechazado, por ejemplo uno caducado. - Averigua qué token falló antes de cambiar ninguna clave. Si las páginas sin sesión funcionan y las que tienen sesión no, es una sesión y no tu clave de API.
- Revisa los cuatro sitios donde se esconde la clave antigua: los ajustes de la herramienta, el propio código, los secretos de Edge Function y un despliegue que nadie reconstruyó.
- Haz la rotación de una sentada. Cada copia que dejes atrás es una aplicación que funciona a medias, y el sitio en producción sigue enviando la clave antigua hasta que se reconstruya.
- Con las claves de firma nuevas, revocar es el paso que acaba de verdad con las firmas antiguas. Rotar por sí solo las deja aceptadas.
Empieza preguntándote si una página sin sesión todavía carga, porque esa sola respuesta decide qué mitad de este artículo te afecta. Si resulta que había una clave secreta a la vista, rotarla explica en qué orden proceder, y qué claves fueron alguna vez seguras de publicar cuenta por qué las dos acabaron pareciéndose tanto.
Preguntas frecuentes
¿Qué significa PGRST301?
PGRST301 es PostgREST respondiendo HTTP 401 porque no pudo verificar un token firmado que iba en tu petición. PostgREST es el servidor que convierte tus tablas de Supabase en la API HTTP con la que habla tu aplicación. Su propia referencia de errores describe el código como un JWT recibido que no se pudo decodificar o que no es válido, y desde la versión 13 el campo message lleva una frase más concreta sobre qué parte de la comprobación falló.
¿Por qué recibo 401 después de rotar mi clave de Supabase?
Porque en algún sitio se sigue enviando una copia de la clave antigua. Los lugares habituales son los ajustes de proyecto de tu herramienta, una clave escrita directamente en el código, un secreto de Edge Function y un despliegue que se construyó antes del cambio y que nadie ha vuelto a construir. Un token firmado con una clave que el servidor ya no acepta no pasa la verificación, y eso te llega como un 401 en cada petición en vez de como un aviso sobre la clave.
¿En qué se diferencia de No API key found in request?
Son dos mensajes distintos con causas distintas. No API key found in request significa que la petición llegó sin ninguna cabecera apikey y la rechazó la pasarela antes de que se preguntara nada a tu base de datos. Un invalid JWT significa que el token sí estaba y su firma no coincidió. Lo primero es una carta sin dirección, lo segundo una carta con el lacre equivocado.
¿Tengo que actualizar también mis Edge Functions?
Sí, y es la copia que más se olvida, porque una Edge Function guarda sus propios secretos, separados de los que usa tu frontend. Supabase también advierte de que una función con el ajuste Verify JWT activado puede romperse al rotar una clave de firma, y de que cualquier componente que verifique los tokens por su cuenta contra el secreto JWT antiguo se romperá por el mismo motivo.
¿Cómo veo qué clave envía realmente mi aplicación en producción?
Abre tu sitio publicado, no la vista previa, y mira qué envía la página de verdad. Una clave antigua de Supabase es una cadena larga que empieza por eyJ, y las nuevas empiezan por sb_publishable_ o sb_secret_. Nuestro escaneo gratuito lee una aplicación en producción como lo haría un desconocido y te dice qué claves se ven desde fuera, lo que también revela si todavía se está enviando una antigua.