Cuando la autenticación en Docker falla en móviles: un error multiplataforma
El aviso llegó un jueves por la tarde: «Google Sign-In no funciona en dispositivos móviles». El hilo de soporte era breve: tres usuarios, todos con iOS, describían el mismo indicador de carga de OAuth que no llevaba a ninguna parte. La aplicación web funcionaba. En computadoras de escritorio también. Solo fallaba en dispositivos móviles, y únicamente al iniciar sesión con OAuth. La autenticación con correo electrónico y contraseña seguía funcionando sin problemas.
Es el tipo de informe de error que promete una solución rápida y acaba consumiendo una tarde de confusión. Los síntomas —un flujo de autenticación que falla en una sola plataforma— apuntan simultáneamente en una docena de direcciones: URI de redirección, CORS, intercambio de tokens, almacenamiento de sesiones, configuración del cliente. Todas eran plausibles. Ninguna resultó ser el problema.
El stack
La aplicación utiliza una instancia autoalojada de Supabase: el stack completo de Docker Compose, desplegado en un servidor dedicado. La arquitectura de Supabase separa la autenticación en un microservicio propio: GoTrue, que gestiona los flujos OAuth, la emisión de tokens y los usuarios. GoTrue se comunica directamente con PostgreSQL mediante una conexión a la base de datos y lee y escribe en el esquema auth. El frontend es una aplicación React Native que se comunica con Supabase mediante la biblioteca cliente estándar de JavaScript.
Es una configuración relativamente habitual en equipos que prefieren controlar sus datos. Supabase autoalojado proporciona la base de datos PostgreSQL completa, la capa de autenticación, las suscripciones en tiempo real y el servicio de almacenamiento, todo en contenedores que usted administra. La contrapartida —y este incidente es un ejemplo preciso— es que los servicios gestionados se ocupan discretamente de detalles de configuración que se convierten en fallos visibles cuando los administra por su cuenta.
Primera hipótesis: discrepancia en la URI de redirección
La investigación de errores de OAuth en dispositivos móviles casi siempre empieza por las URI de redirección. Las apps nativas utilizan esquemas de URL personalizados (myapp://auth/callback) en lugar de URL HTTPS. Una discrepancia entre el callback registrado y lo que espera el proveedor de OAuth produce exactamente el fallo silencioso que observábamos: se inicia el flujo OAuth, se abre el navegador, el usuario se autentica y el callback nunca se completa.
Verificamos las URI de redirección en Google Cloud Console. Revisamos los ajustes del proyecto de Supabase y la configuración de enlaces profundos de la app React Native. Todo coincidía. Las URI estaban registradas correctamente en ambos lados. La pantalla de consentimiento de OAuth se completaba sin errores: Google aceptaba el inicio de sesión. El fallo ocurría después del callback, en algún punto del intercambio de tokens.
Segunda hipótesis: CORS
Los encabezados de Cross-Origin Resource Sharing son el segundo recurso habitual al depurar OAuth en dispositivos móviles. Cuando una app móvil hace solicitudes a un backend con un dominio personalizado, la configuración de CORS determina si el modelo de seguridad del navegador permite la solicitud. Una política de CORS mal configurada produce errores que pueden parecer idénticos a un fallo de autenticación.
Sin embargo, son los navegadores los que aplican CORS; las solicitudes que una app nativa envía mediante su propio cliente HTTP no están sujetas a estas restricciones. Y el síntoma —la autenticación por correo funcionaba mientras que OAuth fallaba— no era coherente con un fallo de CORS. Ambos utilizan el mismo endpoint de GoTrue. Si CORS rechazara las solicitudes, lo más probable es que afectara a ambas, no solo a OAuth. Añadimos registros detallados al cliente móvil. Las solicitudes llegaban a GoTrue. El servicio respondía. Los códigos de estado eran 500.
Lectura de los registros reales
Los registros de los contenedores son donde la depuración de servicios autoalojados más se diferencia de la de servicios gestionados. En Supabase Cloud, el panel muestra registros estructurados de todos los servicios. Con Docker autoalojado, se obtienen directamente de cada contenedor:
docker logs supabase-auth --since 1h 2>&1 | grep -i error
El contenedor de GoTrue llevaba registrando errores continuamente. La línea relevante era:
level=error msg="Error creating oauth state" error="ERROR: function gen_random_uuid() does not exist (SQLSTATE 42883)"
No era un error de redirección de OAuth, sino un fallo de resolución de funciones de PostgreSQL. En las versiones de PostgreSQL anteriores a la 13, la función gen_random_uuid() la proporciona únicamente la extensión pgcrypto, una extensión estándar de PostgreSQL que Supabase instala en el esquema extensions en lugar del esquema predeterminado public. A partir de PostgreSQL 13, también está integrada en el núcleo y siempre se puede resolver, por lo que este error concreto apunta a una versión principal anterior. GoTrue llamaba a gen_random_uuid() sin especificar el esquema y esperaba que PostgreSQL la encontrara mediante la ruta de búsqueda. No la encontraba.
Qué hace search_path
PostgreSQL resuelve los nombres de funciones y tablas sin calificar buscando en una secuencia ordenada de esquemas. El valor predeterminado es "$user", public: primero busca en un esquema con el nombre del usuario actual de la base de datos y después en public. Si una función está en otro esquema y su nombre no incluye explícitamente el esquema —extensions.gen_random_uuid() en lugar de gen_random_uuid()—, PostgreSQL devuelve un error «does not exist», aunque la función exista.
El despliegue gestionado de Supabase configura el search_path de PostgreSQL para incluir extensions, auth y public. Esta configuración está documentada en el código de Supabase, pero no se destaca en la guía de autoalojamiento. En Supabase gestionado se aplica automáticamente. En Docker autoalojado depende de las variables de entorno y los scripts de inicialización aplicados. En nuestra instalación no se había aplicado ninguna de las dos opciones.
Por qué fallaba en móviles y no en la web
La explicación de los registros no responde de inmediato a una pregunta: ¿por qué funcionaba la autenticación web mientras fallaba en dispositivos móviles? La respuesta está en las diferencias entre los flujos OAuth de ambos entornos.
Las aplicaciones web que utilizan OAuth de Supabase emplean de forma predeterminada el flujo implícito: el servidor de autorización devuelve los tokens directamente en el fragmento de la URL tras el callback de OAuth. El cliente los extrae de la URL y los almacena, y la sesión se establece sin un paso separado de intercambio de tokens. Esta ruta de ejecución no llamaba a gen_random_uuid().
Las aplicaciones móviles que siguen las prácticas de seguridad actuales utilizan PKCE (Proof Key for Code Exchange). En PKCE, el cliente genera un verificador de código aleatorio, calcula su hash para producir un desafío de código y envía el desafío al servidor de autorización. Tras el callback, el cliente envía el verificador original al endpoint de tokens, que lo comprueba antes de emitirlos. Ese flujo PKCE era la ruta que fallaba. GoTrue llamaba a gen_random_uuid() al generar el estado de OAuth para el flujo PKCE, y el esquema extensions no estaba en la ruta de búsqueda.
El flujo implícito de la web evitaba esa ruta de ejecución concreta. PKCE en dispositivos móviles no. Una diferencia sutil en la implementación de OAuth puso de manifiesto un error de configuración presente en el stack de Docker desde su despliegue: simplemente no se había ejecutado esa ruta hasta que un cliente móvil intentó utilizar un flujo PKCE.
La solución
La solución correcta es garantizar que el search_path de PostgreSQL incluya todos los esquemas de los que dependen GoTrue y PostgREST. En Docker Compose, lo más sencillo es aplicarlo a nivel de base de datos para que persista con independencia del contenedor que se conecte:
-- Run once against the database
ALTER DATABASE postgres SET search_path TO extensions, auth, public, storage;
ALTER ROLE authenticator SET search_path TO extensions, auth, public, storage;
ALTER ROLE supabase_auth_admin SET search_path TO extensions, auth, public, storage;
Una alternativa es pasar el ajuste mediante la configuración del servicio PostgreSQL en Docker Compose:
services:
db:
image: supabase/postgres:15.1.0.117
command:
- postgres
- -c
- search_path=extensions,auth,public,storage
Aplicamos ambas opciones. Tras reiniciar el contenedor de GoTrue, OAuth se completó sin errores en dispositivos móviles. Apple Sign-In, que habíamos evitado probar durante la depuración, también funcionó de inmediato. Esto confirmó que el problema dependía del flujo y no del proveedor.
Qué revela este incidente sobre la depuración de servicios autoalojados
La investigación, desde «la autenticación falla en móviles» hasta «search_path incorrecto en PostgreSQL», llevó más tiempo del que debería. De ella se desprenden varias lecciones.
Los registros de los contenedores son la fuente de referencia. Investigamos las dos hipótesis incorrectas antes de leer los registros de GoTrue. La primera entrada relevante ya contenía el error exacto. En un stack de Docker autoalojado, leer los registros es el primer paso, no el último recurso tras agotar otras teorías.
Los fallos específicos de una plataforma en un backend compartido casi siempre indican una diferencia en la ruta de ejecución del cliente, no en cómo el backend trata a ese cliente. El backend no sabe si atiende a una app móvil o a un navegador. Sigue la ruta determinada por los parámetros de la solicitud. Entender qué ruta activa PKCE frente al flujo implícito habría señalado de inmediato al estado del flujo PKCE.
Los servicios gestionados y los autoalojados no tienen configuraciones equivalentes. Supabase Cloud aplica detalles de configuración documentados que no se aplican automáticamente en Docker Compose. Cuando algo funciona en Supabase Cloud pero falla en una instalación autoalojada, la diferencia casi siempre está en uno de esos ajustes. El registro de incidencias del repositorio oficial de autoalojamiento suele ser la forma más rápida de localizar el ajuste que falta.
Un error que «solo afecta a una plataforma» suele haber estado presente desde el principio y quedar expuesto únicamente cuando el primer cliente ejecuta una ruta concreta. En este caso, el error de search_path existía desde el día del despliegue del stack de Docker. Hicieron falta meses y una implementación móvil de OAuth para hacerlo visible.