A decisão de arquitetura que a gente não se arrepende (e uma que sim)
Toda base de código honesta tem duas listas: as decisões que envelheceram bem e as que voltaram para cobrar juros. Quem só conta a primeira lista está vendendo alguma coisa. Aqui vão duas decisões reais do ClubPetro — uma que a gente refaria sem pensar, e uma que ainda dói. O objetivo não é bater no peito nem no próprio pé, é mostrar como a gente pensa tradeoff.
A que deu certo: lógica de negócio pura, isolada do resto
Desde cedo a gente cravou uma regra: toda lógica de negócio mora em packages/domain, como função pura — sem banco, sem HTTP, sem I/O. Entra dado, sai decisão. As procedures tRPC orquestram; elas buscam o dado, chamam o domínio, devolvem o resultado. Elas não calculam.
Na hora, isso pareceu cerimônia demais. Por que separar a regra de "uma rede está em risco" da procedure que mostra a rede na tela? Não seria mais rápido calcular ali mesmo?
Seria mais rápido de escrever. Seria muito mais caro de manter. O que essa separação nos deu:
- A regra de negócio virou legível. Quando alguém pergunta "como a gente decide que um posto está saudável?", a resposta é uma função com nome, não uma query de quarenta linhas enterrada num handler.
- A mesma regra roda em lugares diferentes. A definição de risco é usada na tela do CSM, num cron do Trigger.dev que recalcula em lote, e num agente de IA que resume a carteira. Uma fonte de verdade, três consumidores.
- A IA acerta mais. Quando o domínio é função pura, pedir para o Claude ajustar uma regra é seguro — ela não tem efeito colateral escondido, não mexe no banco por acidente. O raio de dano de uma mudança é pequeno e visível.
Essa foi a decisão que mais pagou dividendos. A camada de orquestração fica burra de propósito, e a inteligência fica num lugar só.
A que deu errado: confiar que o schema TypeScript era o banco
Agora a que morde. O ClubPetro usa Drizzle ORM sobre Postgres no Supabase, com dezenas de tabelas. A gente assumiu, por muito tempo, uma coisa que parecia óbvia: que o schema TypeScript do Drizzle refletia o banco real.
Não reflete. As migrations aqui são SQL escrito à mão, e ao longo do tempo o banco derivou do código. Colunas que existem em produção e não estão no schema TS. Campos que o TypeScript jura que são de um jeito e no banco são de outro. Um caso clássico: uma tabela de registros financeiros ganhou colunas de NFSe direto no banco que o schema nunca soube que existiam; outra tinha um campo type cujos valores reais não batiam com o enum declarado — filtrar pelo valor "certo" retornava zero linhas.
O custo desse drift é insidioso porque o código compila. O typecheck passa. Tudo parece verde. E aí a query devolve vazio, ou o db:push ameaça reescrever uma tabela e apagar colunas que o TS não conhece. O bug não aparece no editor; aparece em produção, com dado de cliente.
O que a gente aprendeu na marra:
- Nunca rodar
db:pushnuma tabela com drift conhecido — ele sincroniza o banco com o TS e destrói o que o TS não vê. - Conferir a tabela real antes de escrever query nova, via Studio ou SQL direto, em vez de confiar no tipo gerado.
- Tratar o schema TS como um mapa aproximado, não como o território. Útil para orientação, perigoso como fonte de verdade.
Se a gente refizesse, teria investido cedo numa forma de detectar drift automaticamente — um check no CI que compara schema declarado com banco real. A dívida técnica que a gente escolheu ignorar "por enquanto" virou uma classe inteira de bugs sutis.
O que essas duas decisões têm em comum
As duas nasceram de um tradeoff entre velocidade agora e custo depois. Na primeira, a gente pagou o custo cedo — a cerimônia de isolar o domínio — e colheu depois. Na segunda, a gente adiou o custo — deixou o drift crescer — e pagou com juros.
A lição não é "sempre pague cedo". Às vezes adiar é a escolha certa; nem toda dívida técnica precisa ser quitada. A lição é ser honesto sobre qual tipo de dívida você está assumindo e revisitar a aposta quando o contexto muda. Drift de schema era barato num banco com cinco tabelas. Ficou caro com cinquenta.
O builder que se dá bem aqui é o que consegue segurar essas duas verdades ao mesmo tempo: defender com convicção a arquitetura que funciona e admitir sem drama a que não funcionou. Arquitetura não é sobre estar certo no papel. É sobre estar honesto com o que o código te ensinou.