O plano perfeito que foi rejeitado em 20 segundos
Como gastei dois dias projetando uma migração de identidade — e o CTO escolheu a opção que planejei em meia hora.
Segunda-feira, reunião curta: "vamos migrar os usuários do sistema legado pro novo. Aqui está o código legado, vê como deixar os dois compatíveis." Três dias depois eu tinha um plano detalhado — POC, script idempotente, import em lote pensado pra milhões de contas, tratamento de cada cenário de falha. Na apresentação, meu CTO olhou, ouviu, e escolheu a outra opção — a que eu tinha rascunhado em 30 minutos — em 20 segundos.
Esse texto é sobre por que ele estava certo, e sobre os sinais de parada que eu ignorei no caminho porque tinha me apaixonado pela solução difícil.
o pedido
Os requisitos que me passaram eram claros:
- Verificar a possibilidade de usar Keycloak (o default de quase todos os projetos da empresa).
- Tudo no banco estava criptografado — eu precisava verificar a descriptografia.
- Login com Google e Apple eram requisitos funcionais.
- Migrar 100% dos usuários.
Nada disso, sozinho, era assustador. O problema foi o que eu fiz com "verificar a possibilidade".
A investigação (a parte que valeu a pena)
Comecei mapeando as tabelas de usuário do legado. Descoberta número um: eram 6 bancos de dados diferentes. Quis entender o porquê, mas ninguém da equipe que projetou aquilo ainda trabalha na empresa — o conhecimento foi embora com as pessoas. (Se você precisa de um argumento pra escrever ADR, é esse: o porquê de uma decisão evapora junto com quem a tomou.)
Apesar disso, os campos eram bem comportados: mapear e deixar os dois esquemas compatíveis foi mais fácil do que eu esperava. Essa parte não foi desperdício — foi ela que de-riscou a decisão toda. Guarda isso, porque é o único pedaço de rigor que se pagou.
Design it twice
Não parei no primeiro desenho. Abri duas opções.
Opção A — auth interna. A gente cria o login próprio, cuida de sessão, refresh, e integra Google e Apple na mão. Migração trivial: os hashes bcrypt do legado continuam funcionando, é só ligar. Contra: quebra o padrão da empresa (Keycloak em todo lugar) e tem cara de "simples demais". Pra amenizar, propus uma migração contínua pro Keycloak no futuro: padronizar os hashes de bcrypt pra argon2, com os mesmos parâmetros do Keycloak. Planejei tudo isso em uns 30 minutos.
De início eu achei a A genial. Mas não dá pra parar no primeiro desenho — "design it twice": precisava redesenhar do zero. Então fui pro segundo, o que eu achava que seria o certo.
Opção B — Keycloak direto. Migrar todos os usuários pra dentro do Keycloak de uma vez. Comecei listando o que o Keycloak precisava e o que faltava. E aqui vieram os sinais.
Os três sinais de parada que eu ignorei
Sinal 1 — Keycloak não tem hash bcrypt nativo. Pra uma peça do tamanho dele, achei bizarro. O padrão dele é PBKDF2 (e argon2 nas versões mais novas). Pra importar hash bcrypt você precisa escrever/instalar um PasswordHashProvider via SPI — código de terceiro, e as poucas alternativas que existem não são muito mantidas.
E tem um detalhe que muita gente esquece: bcrypt é one-way. "Padronizar bcrypt → argon2" não é uma conversão em lote. Você só consegue rehashear no próximo login válido, quando tem a senha em texto puro na mão pra verificar no bcrypt e regravar em argon2. Quem nunca mais logar continua em bcrypt — uma cauda que, no dia do cutover pro Keycloak, ainda vai exigir aquele SPI ou um reset de senha forçado.
Sinal 2 — plugin é escopo de instância, não de realm. SPI/provider no Keycloak é deployado no servidor inteiro, no classpath da instância. Você não instala um provider só pra um realm. Se você é multi-tenant com um realm por cliente, isso é uma faca no pé.
Sinal 3 — login com Apple quase não funciona direto. O "Sign in with Apple" é OIDC-ish, mas com manhas: o client secret é um JWT que você tem que gerar e rotacionar (assinado com sua chave privada da Apple, ES256), e a Apple só devolve nome/e-mail no primeiro consentimento. O provider OIDC genérico do Keycloak não gera nem rotaciona esse JWT sozinho. Resultado: mais um SPI custom (identity provider), de novo dependendo de extensão da comunidade.
Eu deveria ter parado exatamente aqui. Três sinais, todos apontando pra "o Keycloak resiste a esse caso". Mas eu achava a solução brilhante. Então gastei mais um dia inteiro nela: script de migração, POC pra ver como o Keycloak se comporta num import em lote na casa dos milhões de contas, idempotência, safe re-run, tratamento de cada cenário. Terminei com um plano lindo. Estava orgulhoso.
O que aquele dia produziu (e por que já vinha rachado)
O plano da Opção B era bonito no papel. Deixa eu abrir o que tinha dentro — e onde cada peça já vinha trincada.
O "rolling release" disfarçado. A ideia era não fazer big-bang. O app checava "esse usuário já existe no Keycloak?"; se não, no primeiro login válido a gente provisionava ele lá (migração just-in-time), e quem nunca logasse ficava pra trás — a cauda que essa abordagem sozinha não resolve. Vender isso como "rolling release" soa maduro, mas no fundo era migração preguiçosa por usuário embrulhada num nome de estratégia de deploy. O custo escondido: durante a janela inteira você mantém duas fontes de verdade sobre a autenticação — o legado e o Keycloak — e toda a lógica de "de quem é a verdade agora" mora no app.
Os plugins (SPI) não saem de graça. Os dois SPI que os sinais exigiam — o PasswordHashProvider de bcrypt e o identity provider da Apple — viram um JAR que você compila, joga em providers/ e sobe junto com o servidor. Três consequências que eu tinha subestimado:
- Ficam presos à versão do Keycloak. Todo upgrade vira uma aposta de que o JAR de terceiro ainda compila e roda.
- São escopo de instância: afetam todos os realms daquele Keycloak, não só o meu. Raio de explosão bem maior que o meu projeto.
- Um deles (o do bcrypt) é código pouco mantido que eu herdaria a manutenção sem querer.
A alternativa: import em lote. O outro caminho era o oposto do lazy — um script que joga todos os usuários pro Keycloak de uma vez, resolvendo a cauda que o rolling release deixava. Como eu não tinha acesso ao dump real, montei um seed caseiro de 100k contas pra ver como o Keycloak se comportava no import em lote e extrapolar pra escala real, na casa dos milhões: o que acontece quando o import falha no meio, como garantir idempotência pra re-rodar sem duplicar, como reconciliar o que já entrou. Esse script é one-use — roda uma vez e morre, então não é dívida de manutenção. Mas era mais um dia inteiro de rigor investido numa opção que eu já deveria ter matado.
O nome disso é efeito IKEA: eu valorizei o Plano B porque construí ele, não porque era melhor. E foi essa afeição que me fez silenciar os três alarmes.
Os 20 segundos
Na apresentação pro CTO, mostrei a A. Ele gostou. Mas eu — pleno querendo virar sênior — recomendei a B e detalhei tudo. Passo a passo, cenário por cenário, um "rolling release" disfarçado, digno de Oscar.
E é aqui que acontece a única coisa que eu faria de novo: enquanto eu explicava em voz alta, a ficha caiu. Traçar o plano inteiro na velocidade da fala, pra outra pessoa, é o detector de complexidade mais barato que existe. No meio da minha própria apresentação eu já estava sentindo o peso do que tinha desenhado.
O CTO ouviu tudo e foi direto: ficaríamos com a A, mais simples e melhor, e a migração futura ficava pra depois. Argumentei sobre débito técnico — pra ele não era problema, complexo demais pra pagar agora. Pedi pra pelo menos deixar preparado — usar argon2 desde já e separar o identity provider atrás de uma interface. Ele topou na hora.
O que a gente manteve (o débito que se compra barato)
O acordo não foi "nunca Keycloak". Foi comprar a opção de migrar depois, sem pagar o preço agora:
- Hashes novos já em argon2id, com parâmetros compatíveis com o Keycloak. No dia do cutover, esses não precisam de nenhum hash-provider custom.
- Rehash preguiçoso no login: bcrypt vira argon2id conforme os usuários logam.
- Identity provider atrás de uma interface estreita, pra trocar a auth interna pelo Keycloak sem espalhar a mudança pelo código.
Honestidade: sobra a cauda de quem nunca loga. Esses ainda vão precisar de SPI de bcrypt ou reset no cutover. Mas isso é um problema pequeno, adiado, e conhecido — não um débito escondido.
O que eu levo
- Rigor na opção errada não é rigor, é desperdício. POC, idempotência, safe re-run — tudo isso é sênior. Mas eu apliquei antes de escolher a opção. Escolhe primeiro, aprofunda depois.
- Sinal de parada existe pra atualizar sua aposta. Três "isso resiste" seguidos não são obstáculos pra vencer no braço; são o design te dizendo não.
- Explica seu design em voz alta cedo — antes de afundar dois dias. Foi falando que eu senti o peso. Se eu tivesse apresentado os dois desenhos crus no dia 1, teria economizado o dia 2.
- Sênior é escolher o chato e comprar opção barata pro futuro. O CTO fez em 20 segundos o que eu não fiz em três dias: separar "o que resolve o problema agora" de "o que me impressiona".
A investigação valeu. O plano da opção que eu já deveria ter matado, não. Saber diferenciar os dois é, no fim, a parte sênior de tudo isso.