Quando a Autenticação Docker Quebra o Login Mobile: Uma História de Bug Cross-Platform
O ticket chegou em uma tarde de quinta-feira: “Sign-In com Google não está funcionando no mobile.” O thread de suporte era curto – três usuários, todos iOS, todos relatando o mesmo spinner OAuth que não levava a lugar nenhum. O aplicativo web funcionava. O desktop funcionava. Apenas o mobile falhava, e apenas para sign-in OAuth. A autenticação por e-mail e senha continuava sem problemas.
Este é o tipo de relatório de bug que promete uma correção rápida e entrega uma tarde de confusão. Os sintomas superficiais – um fluxo de autenticação quebrado em uma plataforma – apontam em uma dúzia de direções simultaneamente. URIs de redirecionamento. CORS. Troca de token. Armazenamento de sessão. Configuração do lado do cliente. Cada um é plausível. Nenhum resultou ser o problema.
A stack
O aplicativo roda em uma instância Supabase auto-hospedada – a stack completa do Docker Compose, implantada em um servidor dedicado. A arquitetura do Supabase separa a autenticação em seu próprio microsserviço: GoTrue, que lida com fluxos OAuth, emissão de tokens e gerenciamento de usuários. O GoTrue se comunica com o PostgreSQL via uma conexão direta ao banco de dados, lendo e escrevendo no schema auth. O frontend é um aplicativo React Native comunicando com o Supabase via a biblioteca cliente JavaScript padrão.
Esta é uma configuração relativamente comum para equipes que preferem controlar seus dados. O Supabase auto-hospedado fornece o banco de dados PostgreSQL completo, a camada de autenticação, assinaturas em tempo real e o serviço de armazenamento, todos rodando em contêineres que você gerencia. A troca – e este incidente é um exemplo preciso disso – é que os serviços gerenciados lidam silenciosamente com detalhes de configuração que se tornam falhas visíveis quando você os gerencia sozinho.
A primeira hipótese: incompatibilidade de URI de redirecionamento
Erros OAuth no mobile quase sempre começam com URIs de redirecionamento. Aplicativos nativos usam esquemas de URL personalizados (myapp://auth/callback) em vez de URLs HTTPS, e uma incompatibilidade entre o callback registrado e o que o provedor OAuth espera produz exatamente o tipo de falha silenciosa que estávamos vendo: o fluxo OAuth inicia, o navegador abre, o usuário autentica e o callback nunca é concluído.
Verificamos a configuração do URI de redirecionamento no Google Cloud Console. Verificamos as configurações do projeto Supabase. Verificamos a configuração de deep-link do aplicativo React Native. Tudo correspondia. Os URIs estavam corretamente registrados em ambos os lados. A tela de consentimento OAuth foi concluída sem erro – o Google estava aceitando o sign-in. A falha estava acontecendo após o callback, em algum lugar na troca de token.
A segunda hipótese: CORS
Os headers de Cross-Origin Resource Sharing são o segundo reflexo para depuração de OAuth mobile. Quando um aplicativo mobile faz requisições para um backend rodando em um domínio personalizado, a configuração CORS determina se o modelo de segurança do navegador permite a requisição. Uma política CORS mal configurada produz uma classe de erros que pode parecer idêntica a uma falha de autenticação.
Mas na realidade, o CORS opera na camada de rede antes que a lógica de autenticação rode. E o sintoma – que a autenticação por e-mail funcionava enquanto o OAuth não funcionava – era inconsistente com uma falha CORS. Ambos usam o mesmo endpoint GoTrue. Se o CORS estivesse rejeitando requisições, rejeitaria todas elas, não seletivamente. Adicionamos registro detalhado ao cliente mobile. As requisições estavam chegando ao serviço GoTrue. O serviço estava respondendo. Os códigos de status eram 500.
Lendo os logs reais
Os logs de contêiner são onde a depuração auto-hospedada diverge mais nitidamente dos serviços gerenciados. Com o Supabase Cloud, o dashboard expõe logs estruturados de cada serviço. Com o Docker auto-hospedado, você puxa os logs de contêineres individuais diretamente:
docker logs supabase-auth --since 1h 2>&1 | grep -i error
O contêiner GoTrue estava registrando erros continuamente. A linha relevante:
level=error msg="Error creating oauth state" error="ERROR: function gen_random_uuid() does not exist (SQLSTATE 42883)"
Isso não era um erro de redirecionamento OAuth. Era uma falha de resolução de função PostgreSQL. gen_random_uuid() é fornecida pela extensão pgcrypto – uma extensão PostgreSQL padrão instalada pelo Supabase no schema extensions em vez do schema padrão public. O GoTrue estava chamando gen_random_uuid() sem qualificador de schema, esperando que o PostgreSQL a encontrasse via o search path. Não estava encontrando.
O que o search_path faz
O PostgreSQL resolve nomes de funções e tabelas não qualificados pesquisando uma sequência de schemas em ordem. O padrão é "$user", public: primeiro procure em um schema nomeado pelo usuário atual do banco de dados, depois em public. Se uma função vive em qualquer outro schema e não está explicitamente qualificada com seu nome de schema – extensions.gen_random_uuid() em vez de gen_random_uuid() – o PostgreSQL retorna um erro “não existe” independentemente de a função realmente existir.
O deployment gerenciado do Supabase configura o search_path do PostgreSQL para incluir extensions, auth e public. Essa configuração está documentada na base de código do Supabase, mas não é destacada prominentemente no guia de auto-hospedagem. No Supabase gerenciado, ela é aplicada automaticamente. No Docker auto-hospedado, depende de quais variáveis de ambiente e scripts de inicialização foram aplicados. Os nossos não tinham nenhum.
Por que mobile e não web
A questão que a explicação dos logs não responde imediatamente: por que a autenticação web funcionava enquanto o mobile falhava? A resposta está em como os fluxos OAuth diferem entre ambientes.
Os aplicativos web que usam OAuth do Supabase têm como padrão o fluxo implícito: o servidor de autorização retorna tokens diretamente no fragmento de URL após o callback OAuth. O cliente os extrai da URL, os armazena e a sessão é estabelecida sem uma etapa separada de troca de token – um caminho de código que por acaso não chamava gen_random_uuid().
Os aplicativos mobile que seguem as melhores práticas de segurança atuais usam PKCE: a Prova de Chave para Troca de Código. No PKCE, o cliente gera um verificador de código aleatório, o hash para produzir um desafio de código e envia o desafio ao servidor de autorização. Após o callback, o cliente envia o verificador de código original ao endpoint de token, que o verifica antes de emitir tokens. Essa etapa de troca de token era o caminho de código que estava falhando. O GoTrue estava chamando gen_random_uuid() ao gerar o estado OAuth para o fluxo PKCE, e o schema extensions não estava no search path.
O fluxo implícito na web contornou esse caminho de código específico. O PKCE no mobile não. Uma diferença sutil na implementação OAuth expôs um erro de configuração que existia na stack Docker desde o deployment – simplesmente não havia sido exercido até que um cliente mobile tentasse um fluxo PKCE.
A correção
A correção correta é garantir que o search_path do PostgreSQL inclua todos os schemas dos quais o GoTrue e o PostgREST dependem. Em uma configuração Docker Compose, isso é mais limpo quando aplicado no nível do banco de dados para que persista independentemente de qual contêiner se conecta:
-- Execute uma vez no banco de dados
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;
Uma alternativa é passar a configuração via a configuração do serviço PostgreSQL no Docker Compose:
services:
db:
image: supabase/postgres:15.1.0.117
command:
- postgres
- -c
- search_path=extensions,auth,public,storage
Aplicamos ambos. Após reiniciar o contêiner GoTrue, o OAuth mobile foi concluído sem erro. O Sign-In com Apple – que havíamos evitado testar durante o processo de depuração – também funcionou imediatamente, confirmando que o problema era específico ao fluxo e não ao provedor.
O que este incidente revela sobre depuração auto-hospedada
O caminho de depuração – de “auth quebrado no mobile” a “search_path errado no PostgreSQL” – levou mais tempo do que deveria. Vale a pena extrair várias lições.
Os logs de contêiner são a fonte da verdade. Ambas as hipóteses incorretas foram perseguidas antes de ler os logs do GoTrue. Os logs continham o erro exato na primeira entrada relevante. Em uma stack Docker auto-hospedada, ler os logs é o primeiro passo, não um último recurso após esgotar outras teorias.
Falhas específicas de plataforma em um backend compartilhado quase sempre indicam uma diferença no caminho de código do cliente, não uma diferença em como o backend trata aquele cliente específico. O backend não sabe que está atendendo a um aplicativo mobile versus um navegador. Ele segue o caminho de código determinado pelos parâmetros da requisição. Entender qual caminho de código o PKCE ativa – em oposição ao fluxo implícito – teria apontado para a troca de token imediatamente.
Os serviços gerenciados e os serviços auto-hospedados não são configurações equivalentes. O Supabase Cloud aplica detalhes de configuração que estão documentados, mas não são automaticamente aplicados na configuração do Docker Compose. Quando algo funciona no Supabase Cloud, mas falha auto-hospedado, a lacuna está quase sempre em uma dessas configurações. O rastreador de problemas do repositório oficial auto-hospedado é frequentemente a maneira mais rápida de localizar a configuração específica que está faltando.
Um bug que “só afeta uma plataforma” é frequentemente um bug que sempre esteve presente, exposto apenas pelo primeiro cliente a exercer um caminho de código específico. Neste caso, o erro de search_path existia desde o dia em que a stack Docker foi deployada. Levou meses e uma implementação de OAuth mobile para torná-lo visível.
Insights Relacionados
Artigos relacionados
Mais de Cem Contêineres Docker: Nossa Rotina Mensal de Verificação de Saúde
Implantando Aplicações React em Produção: Configuração Completa com Docker e Proxy Reverso Traefik
Construindo uma Stack de Desenvolvimento Multi-Tenant com Docker: Configuração Completa para Implantações Escaláveis de Clientes