CRONOS

O que é

O CRONOS é uma plataforma de controle de despesas compartilhadas. Ela permite registrar as despesas de um grupo de pessoas e ratear a conta no final do mês. Escrevi a aplicação inteira, front-end, API e banco, e também sou responsável pela infraestrutura que a mantém no ar.

Contexto

Esse é o terceiro sistema que escrevo para o mesmo problema. Comecei com um sistema financeiro de mesa em Java com Swing, para controlar despesas compartilhadas. O CRONOS é a mesma ideia levada para a web e depois para a nuvem. A pergunta é sempre a mesma, “quem deve pra quem”, e ela já atravessou três arquiteturas diferentes.

Na nuvem, o orçamento é apertado: uma instância pequena, memória limitada, sem verba para escalar horizontalmente. Qualquer decisão de infraestrutura precisava caber nesse espaço.

O problema

Rodar uma aplicação Next.js, uma API Node.js e um PostgreSQL com pouca RAM, sem que o banco ou o processo Node estourem memória em produção. E fazer isso com CI/CD, publicando imagem a cada push, em vez de deploy manual.

Como resolvi

Escolhi ECS sobre EC2 em vez de Fargate, migrei a instância para ARM64 (t4g.small), coloquei uma borda em Caddy na frente da aplicação e configurei o GitHub Actions para rodar a suíte de testes e publicar a imagem no GHCR a cada push. O build do front-end usa o modo standalone do Next.js, que reduz bastante o que efetivamente sobe para o registry.

Como eu sei que funciona

O sistema mexe com dinheiro entre pessoas que moram juntas. Um rateio errado significa alguém pagando a conta do outro. Por isso a suíte de testes cobre as três camadas da pirâmide, e nada vai para produção sem passar por ela.

Na API, são 34 arquivos de teste em Jest. Os 19 unitários cobrem os schemas Zod, as regras dos services e o rateio propriamente dito. Os 15 de integração usam Supertest contra a aplicação, com um PostgreSQL subido como serviço no runner. Eles exercitam auth, residências, despesas, notificações, usuários e acertos de pagamento, além dos casos de segurança: rate limiting, reúso de refresh token, troca e recuperação de senha e emissão dos eventos de segurança.

Email e armazenamento de comprovantes são portas injetáveis, trocadas por fakes em memória na suíte, então nenhum teste automatizado abre conexão com a AWS. Os limitadores de taxa ficam desarmados em NODE_ENV=test, porque a suíte dispara dezenas de requisições na mesma rota de propósito. Armados, eu estaria testando o limitador em vez do endpoint. Existe um gancho para armá-los nos testes que são especificamente sobre rate limiting.

No front-end, são 46 suítes e 294 testes em Jest com Testing Library, sobre Server Actions, hooks e formulários. Antes de escrever teste novo, precisei consertar a métrica. O jest.config.ts não tinha collectCoverageFrom, então a cobertura só contava arquivos já importados por algum teste e reportava 68%, um número que subia quanto menos código eu testasse. Com o denominador correto, a cobertura era 18,4%. Levei para 58,2% seguindo um plano versionado no repositório, que registra também o que ficou de fora e por quê.

Os 16 specs de Cypress cobrem os fluxos que um usuário percorre: criar conta, criar e entrar em residência, lançar, editar e excluir despesa, despesa recorrente, fechar e reabrir o mês, convidar e gerenciar membros, acertos de pagamento, relatórios e recuperação de senha.

Como a entrega é fechada

A entrega segue o modelo build once, promote everywhere. Os repositórios do front-end e da API rodam lint, testes e build, publicam a imagem no GHCR e disparam por repository_dispatch o repositório orquestrador de deploy. Ele sobe a stack inteira com Docker Compose (Postgres, API e front-end), roda o Cypress contra ela e, só se tudo passar, re-taggeia com imagetools aquele artefato exato como :stable.

Nada é reconstruído para produção. A imagem que vai ao ar é a mesma que passou no E2E. Quando um spec falha, screenshots e vídeos sobem como artefato do job. Sem eles, a única informação seria “o teste não passou”, o que não distingue timeout, erro devolvido pela API e front sem conseguir falar com a API.

O que eu troquei pelo quê

Escolhi ECS sobre EC2 em vez de Fargate porque, no tamanho deste sistema, Fargate custava mais que a instância inteira. O preço disso é que eu administro a máquina. Swap, limite de memória do Postgres e o agente ECS são problema meu.

Na cobertura de testes, parei em 58,2% em vez de perseguir um número redondo. Composição de rota, page.tsx e layout.tsx estão fora do denominador porque o comportamento deles já é exercitado pelo Cypress. Testá-los duas vezes encareceria a suíte sem aumentar a confiança.

Resultado

A imagem do front-end caiu de mais de 1 GB para cerca de 390 MB (payload de 47 MB) depois do build standalone. O custo da instância caiu de US$ 15,18 para aproximadamente US$ 6,13 por mês com a migração para t4g. O Postgres roda hoje com teto de 384 MB de memória, o que faz de cada decisão de configuração um exercício de restrição concreta.

Em qualidade, a cobertura de statements do front-end saiu de 18,4% para 58,2%, a API tem 34 arquivos de teste entre unitários e integração com banco real, e 16 fluxos de ponta a ponta rodam contra a stack completa antes de qualquer imagem virar :stable.

O que eu faria diferente

[VOCÊ ESCREVE]

Galeria

  • Painel
    Painel