VEZTA — DOCUMENTO TÉCNICO · REV 03 · OUT 2026● CÓDIGO PRIVADO

[00] Showcase de decisões

Engenharia
do Vezta/

Um app de finanças pessoais e em grupo com mecânicas de RPG, construído por uma pessoa sobre um servidor próprio. O código-fonte é privado; esta página documenta as decisões, o que cada uma rejeitou e o que cada uma custou.

DECISÃO MACROY-STATEMENT · SÍNTESE ARQUITETURAL

No contexto de um app de finanças pessoais e em grupo com dois canais de registro (app Android e bot do Telegram) e carteira de ativos,

enfrentando a necessidade de atender uma meta de 10 mil usuários simultâneos a partir de um único nó auto-hospedado, com RAM contada e sem orçamento para escalar horizontalmente,

decidimos adotar um monólito modular com fronteiras verificadas em compilação, identidade delegada a um IdP próprio e coleta de cotações em workers isolados que se comunicam por eventos,

rejeitando microsserviços, autenticação escrita dentro do domínio e identidade como serviço pago por usuário ativo,

para alcançar caminho de escrita curto e sem salto de rede, revogação de sessão e 2FA sem código de segurança próprio, e falha de fonte externa que não derruba o produto,

aceitando o ônus de escala apenas vertical do núcleo, consistência eventual nas telas de preço e mais um componente com banco próprio para operar.

Ler as decisões ↓Ver a arquitetura
Problema
Controle financeiro só vira hábito se registrar custar segundos. E nenhum app de finanças pessoais resolve despesa dividida em grupo de forma nativa.
Solução
Um domínio único servindo dois canais de registro, com metas, despesa compartilhada em party e progressão por XP sobre o mesmo dado.
Stack
Java 17 · Spring Modulith · Postgres · RabbitMQ · Zitadel · Expo / React Native · Nginx
Verificação
Metas de carga declaradas (p95 < 300 ms, erro < 0,5%) e cenários k6 escritos. Os números medidos entram aqui quando o ambiente de teste for provisionado.

[00] Resumo

Ficha técnica

O Vezta permite registrar gastos pelo app ou pelo Telegram, criar metas de economia e de limite e dividir despesas em grupo (parties). O progresso financeiro vira XP, níveis e conquistas, e a carteira acompanha ações e criptoativos.

Plataforma
Android · Expo / React Native
Backend
Java 17 · Spring Boot
Serviços
5 repositórios
Identidade
Zitadel (OIDC)
Mensageria
RabbitMQ
Infraestrutura
Nó único auto-hospedado
Canais
App · Bot Telegram
Equipe
1 desenvolvedor
Sobre o código

Os repositórios são privados. Esta página mostra decisões, contratos e fluxos; nada aqui expõe segredo ou configuração de produção. O que pode ser auditado, e como, está em [09] Entrega e DX.

[01] Problema e restrições

O que foi dado, antes de escolher qualquer tecnologia

Nenhuma decisão desta página faz sentido sem as restrições abaixo. Elas vieram primeiro, e é delas que sai a justificativa de cada escolha — inclusive das que parecem conservadoras.

RestriçãoO que ela significaO que ela forçou na arquitetura
Usuário finalPessoa física no Brasil que desiste de planilha e do app do banco por fricção, e grupos — casa, viagem, casal — que dividem despesa sem ferramenta própria.Registro em até três toques e um canal de conversa, não apenas telas. Party como entidade de primeira classe, não um relatório compartilhado.
Escala-alvo10 mil usuários simultâneos no lançamento, sem verba para crescer em largura.Consertar a leitura antes de escalar: agregação em SQL, índices e read models vêm antes de qualquer réplica (ADR-005).
InfraestruturaUm nó único auto-hospedado, em hardware próprio sob guarda física, com memória contada. Só a cópia de segurança do banco sai da máquina.Keycloak descartado por consumo de RAM; monólito modular em vez de malha de serviços; sem orquestrador (ADR-001, ADR-002).
ConformidadeLGPD: base legal declarada por finalidade, exclusão de conta feita pelo próprio usuário, PII mínima e nenhum dado bancário.Identidade fora do domínio, exclusão propagada por outbox e zero credencial de banco no sistema — o app não integra open finance de propósito.
EquipeUm desenvolvedor, cinco repositórios, sem revisor par.Fronteira de módulo verificada em compilação e contrato escrito antes do código, porque não existe code review para pegar o erro depois.
Fontes de dadosCotações vêm de APIs públicas com limite de requisições, formato instável e nenhum SLA.Coleta fora do domínio, em workers que podem falhar sozinhos, e preço exibido com a hora da última atualização (ADR-004).

[02] Contexto do sistema

Quem usa, e de que o sistema depende

Nível 1 do modelo C4: os limites do sistema e o ecossistema externo, sem uma única menção a tecnologia interna. É a visão que importa antes de abrir qualquer caixa.

FIG 01DIAGRAMA DE CONTEXTO · C4 NÍVEL 1
Diagrama de contexto do sistema VeztaO usuário, uma pessoa física, usa o Vezta diretamente e também conversa com ele pelo Telegram. O sistema Vezta depende de quatro sistemas externos: a plataforma do Telegram para as mensagens, as fontes públicas de mercado (BRAPI, CoinGecko e Binance) para cotações, o Mailjet para e-mail transacional e a Google Play para distribuir o app Android.Usuáriopessoa físicaVeztasistema de finanças pessoaise em grupo, com progressãoregistro · metas · partiesgamificação · carteiraidentidade sob guarda própriaTelegramplataforma de chatFontes de mercadoBRAPI · CoinGeckoBinanceMailjete-mail transacionalGoogle Playdistribuição Androidusa o appfala com o botmensagenscotações
Fig. 01 — A caixa com borda contínua é o sistema sob minha responsabilidade; as tracejadas são sistemas de terceiros. O provedor de identidade aparece dentro do limite porque é auto-hospedado, não contratado. Nenhuma tecnologia interna é mostrada neste nível, de propósito.

[03] Arquitetura

Abrindo a caixa: contêineres

Nível 2 do C4. O domínio fica num monólito modular. O que tem ciclo de vida ou padrão de falha próprio — o bot e a coleta de cotações — roda isolado, e a comunicação assíncrona passa por eventos. Tudo abaixo vive no mesmo nó físico.

FIG 02DIAGRAMA DE CONTÊINERES · C4 NÍVEL 2
Arquitetura de contêineres do VeztaO app e o bot do Telegram chamam o CoreService através do Nginx. O Zitadel emite tokens. Os workers de ações e cripto publicam eventos de preço no RabbitMQ, que o CoreService consome. Todos os contêineres rodam no mesmo nó auto-hospedado, exceto o backup do banco, que é replicado fora da máquina.NÓ ÚNICO AUTO-HOSPEDADO · HARDWARE PRÓPRIO · BRASILAPPExpo / React NativeTelegramServiceSpring Boot · botZitadelIdP · OIDCNginxproxy reversoCoreServicemonólito modulartransações · metasparties · gamificaçãocarteira · catálogooutbox · read modelsPostgres próprioRabbitMQeventos de domínioStockScraperBRAPI · pollingCryptoScraperCoinGecko · Binancebackup cifrado · fora do nóFontes de mercadoAPIs de terceiros
Fig. 02 — Linha ciano: chamadas síncronas (HTTPS). Linha verde tracejada: eventos via RabbitMQ. Tracejado fino: emissão de tokens, coleta externa e replicação de backup. A moldura tracejada marca a fronteira física do nó: tudo dentro dela disputa a mesma RAM, e é essa disputa que explica metade das decisões da seção [05]. Fora dela ficam só o dispositivo do usuário, as APIs de terceiros e o destino do backup.
SVC-01DOMÍNIO

CoreService

Transações, categorias, metas, parties, gamificação, carteira e catálogo de ativos. É um resource server OAuth2: a identidade é o sub do token, não o e-mail.

STACK
Spring Boot · Modulith
DADOS
Postgres · Flyway
SVC-02CLIENTE

APP

Cliente Android com HUD, ledger, cinto de categorias e temas. Gráficos de ativos desenhados em Skia.

STACK
Expo · React Native
GRÁFICOS
victory-native · Skia
SVC-03CANAL

TelegramService

Bot que interpreta mensagens e chama o Core em nome do usuário, com o token dele, via device flow. Máquina de estados de conversa e banco próprio.

STACK
Spring Boot 3 · Java 17
INTEGRAÇÃO
Webhook · OAuth device
SVC-04COLETA

StockScraperService

Consulta cotações na BRAPI e publica asset.price.updated. Raspa nomes do catálogo com Jsoup.

STACK
Spring Boot · Jsoup
AGENDAMENTO
Polling · catálogo 2×/dia
SVC-05COLETA

CryptoScraperService

CoinGecko como fonte primária e Binance como fallback por moeda. Publica crypto.price.updated.

STACK
Spring Boot
DADOS
Postgres próprio

[04] Stack

O que foi escolhido, e o que custou

Toda escolha abaixo trocou algo por outra coisa. A coluna da direita é a parte que costuma ficar fora de README.

CamadaEscolhaPor quêCusto assumido
IdentidadeZitadel auto-hospedadoRefresh/revogação, reset, e-mail verificado, 2FA e login social sem escrever um serviço de auth.Mais um componente (com Postgres próprio) para operar. Keycloak foi descartado pelo consumo de RAM.
BackendMonólito modular (Spring Modulith)Fronteiras verificadas em compilação e eventos pós-commit sem custo de rede.Escala o Core inteiro, não módulos isolados. Aceito para o volume-alvo.
MensageriaRabbitMQ + outboxEntrega confiável entre Core, scrapers e bot, com filas isoladas por consumidor.Consistência eventual: telas precisam tolerar atraso de segundos.
DadosPostgres por serviçoCada serviço migra o próprio esquema (Flyway) sem coordenar deploys.Sem joins entre serviços. Dados compartilhados viajam como evento e viram read model.
ClienteExpo / React NativeUm código, iteração rápida, OTA e ecossistema de gestos e animação.Gráficos com Skia não rodam na web; o cliente web fica fora do escopo atual.
BordaNginxProxy reverso leve, conhecido e suficiente para balancear réplicas do Core.Configuração manual de TLS e rate limit (Caddy automatizaria parte).
Cargak6Metas verificáveis: p95 < 300 ms, erros < 0,5%, teste de queda de réplica.Exige ambiente próximo de produção, provisionado só durante os testes.

[05] Decisões técnicas

Registro de decisões

Formato MADR: contexto, decisão, todas as opções consideradas com o motivo do veredito, e consequências separadas entre ganho e passivo. A ADR-000 está aqui como Substituída, não apagada — a trilha de por que uma decisão caiu vale mais do que a aparência de nunca ter errado.

ADR-000Autenticação escrita dentro do CoreSubstituída pela ADR-001Substituída

Contexto

O Core precisava de login antes de qualquer outra coisa, e escrever a autenticação ali era o caminho mais curto para ter o primeiro endpoint protegido e o app rodando ponta a ponta.

Decisão

Usuário e senha com hash no próprio Core, JWT assinado pelo serviço e o e-mail como chave de identidade do usuário.

Opções consideradas

  • Auth no próprio CoreEscolhida

    Zero componente novo para operar e primeiro endpoint protegido no mesmo dia.

  • Provedor de identidade dedicadoDescartada

    Rejeitada na época: parecia peso desnecessário antes de existir um único usuário real.

  • Identidade como serviço (SaaS)Descartada

    Rejeitada na época: custo recorrente antes de haver receita e antes de o produto existir.

Ganho

  • Caminho mais curto até o app funcionando ponta a ponta, sem infraestrutura adicional.

Custo assumido

  • Sem revogação de refresh, reset de senha, e-mail verificado, 2FA ou login social — cada item viraria código de segurança escrito à mão e mantido por uma pessoa.
  • E-mail como chave de identidade travava a troca de e-mail e inviabilizava login social.
  • Substituída antes do lançamento, ao custo de migrar a chave de identidade de e-mail para sub em todas as tabelas do domínio.
ADR-001Identidade em um IdP, não no CoreSubstitui a ADR-000Aceita

Contexto

A autenticação própria (ADR-000) cobria apenas o caminho felizmente simples. Faltavam refresh com revogação, reset de senha, e-mail verificado, 2FA e login social, e cada um deles viraria código de segurança de responsabilidade minha.

Decisão

Zitadel auto-hospedado com Login UI v2. O Core passa a ser resource server OAuth2: a chave de identidade é o sub, o papel ADMIN vem das claims e o access token dura 10 minutos.

Opções consideradas

  • Zitadel auto-hospedadoEscolhida

    Cobre revogação, 2FA e social de fábrica, roda no nó existente e mantém os dados de identidade sob guarda própria.

  • Keycloak auto-hospedadoDescartada

    Rejeitada pelo consumo de memória: num nó único com RAM contada, ele competiria com o Core e os bancos.

  • Auth0 / Clerk (gerenciado)Descartada

    Rejeitada pelo custo por usuário ativo na meta de escala, e por colocar o dado de identidade fora da infraestrutura própria.

  • Manter o JWT à mãoDescartada

    Rejeitada: era a ADR-000, e o passivo de segurança dela foi exatamente o motivo desta decisão.

Ganho

  • Revogação de sessão, 2FA, reset de senha e login social deixam de ser código meu.
  • Token curto reduz o raio de um vazamento, e o papel vem assinado na claim.

Custo assumido

  • Mais um componente com Postgres próprio para operar e atualizar no mesmo nó.
  • Onboarding passa a ser just-in-time (PROFILE_REQUIRED): existe um usuário autenticado sem perfil de domínio, e toda tela precisa tolerar esse estado.
  • A exclusão de conta deixa de ser um DELETE local: precisa remover a identidade no IdP via outbox, com retentativa.
ADR-002Monólito modular em vez de microsserviçosAceita

Contexto

Havia um ciclo de dependência entre party e transaction, e a gamificação acoplava o cálculo de XP ao caminho de escrita da transação. Ao mesmo tempo, a infraestrutura-alvo é um nó único e a equipe é de uma pessoa.

Decisão

Spring Modulith: módulos com fronteira verificada em compilação, o ciclo quebrado por inversão e a gamificação disparada por evento depois do commit.

Opções consideradas

  • Monólito modular (Modulith)Escolhida

    Fronteira explícita e testável sem pagar latência de rede, deploy múltiplo nem observabilidade distribuída.

  • Dividir o Core em serviçosDescartada

    Rejeitada: no nó único, cada serviço custa RAM e um salto de rede no caminho de escrita, e a operação distribuída não cabe em uma pessoa.

  • Monólito sem fronteira declaradaDescartada

    Rejeitada: o ciclo party ↔ transaction já existia, e sem verificação em compilação ele volta no próximo commit.

Ganho

  • Latência de rede zero entre módulos e um único artefato para implantar.
  • O ciclo de dependência passa a quebrar o build, em vez de ser descoberto em produção.
  • Gamificação pode falhar sem derrubar o registro da transação.

Custo assumido

  • Escala apenas vertical: cresce o Core inteiro, nunca um módulo isolado.
  • Um deploy ruim derruba todo o domínio de uma vez.
  • A garantia de fronteira passa a depender de o Spring Modulith continuar mantido.
ADR-003Telegram vinculado por Device Authorization GrantAceita

Contexto

A primeira versão do bot usava códigos de vínculo, um segredo interno compartilhado com o Core e um JWT assinado pelo próprio bot. Isso fazia do bot uma autoridade de identidade — confiança demais em um componente que lê mensagens de terceiros.

Decisão

O comando /vincular inicia o device flow no Zitadel com um cliente dedicado. O bot guarda o sub e o refresh token cifrado em AES-GCM, e chama o Core com o token do usuário. O Core restringe esse cliente às operações do bot.

Opções consideradas

  • Device Authorization GrantEscolhida

    O bot nunca vê credencial e nunca pode mais do que o usuário que o autorizou; a sessão se estabelece entre a pessoa e o IdP.

  • Códigos de vínculo com segredo compartilhadoDescartada

    Rejeitada: o bot assinava identidade, e vazar o segredo valeria por qualquer usuário.

  • Token de serviço com impersonaçãoDescartada

    Rejeitada: dá ao bot um poder maior que o do usuário, e um comprometimento dele vira acesso total ao domínio.

Ganho

  • Nenhuma senha trafega pelo chat, e o escopo do bot é um subconjunto do escopo do usuário.
  • Revogar o acesso do Telegram passa a ser uma ação no IdP, não um registro que eu preciso apagar à mão.

Custo assumido

  • Renovação de token exige lock de linha e rotação persistida, para duas mensagens simultâneas não invalidarem uma à outra.
  • O usuário precisa sair do chat e abrir o navegador uma vez — fricção real, trocada por não guardar credencial.
ADR-004Cotações coletadas fora do domínioAceita

Contexto

As fontes públicas de preço mudam de formato sem aviso, têm limite de requisições por mês e ficam fora do ar. Se o Core consultasse direto, o limite da fonte viraria o limite do produto.

Decisão

Workers separados coletam e publicam asset.price.updated e catalog.{stock,crypto}.updated. O Core mantém o catálogo e o preço como read model alimentado por esses eventos.

Opções consideradas

  • Workers isolados publicando eventosEscolhida

    A falha de uma fonte fica contida no worker dela, e o Core nunca bloqueia esperando terceiro.

  • API paga de cotaçõesDescartada

    Rejeitada: custo mensal fixo antes de existir receita, para um produto em que o preço é informativo.

  • Coleta sob demanda dentro do CoreDescartada

    Rejeitada: o limite de requisições da fonte passaria a limitar o produto, e cada queda dela viraria erro na tela do usuário.

Ganho

  • Queda ou mudança de formato de uma fonte não derruba o Core.
  • CoinGecko com fallback para Binance por moeda, decidido dentro do worker.

Custo assumido

  • O preço pode atrasar, então a tela precisa exibir a hora da última atualização — e a UI ficou mais complexa por isso.
  • Mais dois serviços, com banco e fila próprios, para operar no mesmo nó.
ADR-005Leitura primeiro: consertar queries antes de escalarAceita

Contexto

A meta é 10 mil usuários simultâneos num único nó auto-hospedado. A Home somava totais carregando entidades, com N+1 causado pelo open session in view.

Decisão

Agregações em SQL, índice composto (party_id, created_at), OSIV desligado, relações LAZY, um endpoint de resumo para a Home e read models mensais. Réplica de leitura só entra se o k6 provar a necessidade.

Opções consideradas

  • Otimizar query, índice e read modelEscolhida

    O gargalo medido estava na forma de ler, não na topologia — o ganho é grande e a complexidade adicionada é pequena.

  • CQRS completoDescartada

    Rejeitada: dois modelos e um barramento de projeção para manter, num produto que ainda não tinha usuário.

  • Cache agressivo desde o inícioDescartada

    Rejeitada: esconde a query ruim em vez de corrigi-la, e cria um problema de invalidação antes de existir um problema de carga.

  • Réplica de leituraDescartada

    Postergada, não rejeitada: entra se e quando o teste de carga mostrar que a leitura saturou o nó.

Ganho

  • A Home deixa de depender do número de transações carregadas em memória.
  • A decisão de escalar passa a ter um gatilho objetivo, em vez de ser tomada por intuição.

Custo assumido

  • Read models mensais exigem invalidação cuidadosa: todo caminho que altera transação tem de invalidar o período certo.
  • O ganho continua sendo meta declarada, não medição: o cenário k6 está escrito, o ambiente de teste ainda não foi provisionado.
ADR-006Compras que falham viram rascunhoAceita

Contexto

Uma compra de ativo pode falhar porque o ticker ainda não entrou no catálogo — uma condição do sistema, não um erro do usuário. Rejeitar nesse caso faz a pessoa perder uma intenção que ela já expressou.

Decisão

Uma operação de compra que falha na validação vira um rascunho em asset_operation_drafts, com retentativa manual, retentativa automática a cada atualização de catálogo, cron diário de segurança, expiração em 30 dias e preservação do executedAt original.

Opções consideradas

  • Rascunho com retentativaEscolhida

    Preserva a intenção e a data original da operação, e resolve sozinho quando o catálogo chega.

  • Rejeitar com erro de validaçãoDescartada

    Rejeitada: transfere para o usuário um problema de catálogo que é do sistema.

  • Fila genérica de retentativaDescartada

    Rejeitada: sem estado visível, o usuário não sabe se a compra existe, falhou ou está esperando.

Ganho

  • O usuário não perde o registro nem a data da operação.
  • Quando o catálogo atualiza, o rascunho se resolve e o bot avisa a pessoa pelo chat.

Custo assumido

  • Mais estados para o cliente exibir e explicar: rascunho, resolvido, expirado.
  • Só a operação de compra gera rascunho — venda continua falhando de forma direta, o que é uma assimetria que o usuário pode achar estranha.
ADR-007Segredos centralizados em um gerenciadorProposta

Contexto

Hoje os segredos vivem em arquivos .env no servidor, com permissão restrita. Funciona para um operador, mas não há rotação, histórico nem separação entre ambientes.

Decisão

Injetar os segredos no momento do deploy via Infisical Cloud, com credencial distinta por banco e portas internas fechadas.

Opções consideradas

  • Infisical CloudDescartada

    Proposta: rotação e histórico sem gastar memória no nó, que é o recurso escasso.

  • Vault auto-hospedadoDescartada

    Rejeitada: mais um serviço com apetite de memória disputando o mesmo hardware.

  • Manter .env no servidorDescartada

    É o estado atual, e o motivo desta proposta existir.

Ganho

  • Rotação de segredo deixa de ser edição manual de arquivo em produção.

Custo assumido

  • Introduz dependência de um serviço externo no caminho do deploy.
  • Ainda exige uma credencial de bootstrap na máquina — o problema diminui, não desaparece.
  • Decidida e ainda não implementada. Está listada aqui porque esconder pendência de segurança é pior do que mostrá-la.

[06] Segurança e conformidade

As perguntas que precisam de resposta exata

Segurança não é um item de lista de recursos; é a primeira coisa que elimina um projeto. Abaixo, as perguntas auditáveis respondidas de forma direta — incluindo a pendência que ainda existe.

Onde os dados moram?
Um Postgres por serviço, em hardware próprio sob guarda física, no Brasil. Nenhum banco tem porta exposta à internet: o acesso acontece pela rede interna do nó. A única cópia que sai da máquina é o backup do banco, cifrado.
Quem acessa, e como esse acesso é controlado?
Um operador — eu. Credencial distinta por banco, sem conta compartilhada entre serviços, e acesso administrativo por chave, não por senha. Nenhum serviço usa superusuário.
Criptografia em trânsito?
TLS em todo o tráfego entre app, bot, borda e serviços. O Nginx termina TLS e nada atravessa a internet em claro.
Criptografia em repouso?
Os refresh tokens do Telegram são cifrados com AES-GCM antes de chegar ao banco. Senha de usuário não existe no domínio: ela nunca sai do provedor de identidade.
Qual é o raio de um token vazado?
O access token dura 10 minutos e é emitido pelo Zitadel, validado pelo Core como resource server. O cliente do bot é limitado às operações do bot, então um token dele não concede acesso administrativo.
Que dado pessoal entra no sistema?
E-mail, o identificador sub e o que o usuário digita. Nenhuma credencial bancária, nenhum acesso a localização, contatos, fotos ou microfone, e nenhum SDK de publicidade. O app não integra open finance de propósito.
Como a exclusão funciona de verdade?
Excluir a conta no app apaga perfil, transações e vínculo do Telegram, e publica pelo outbox um evento que pede a remoção da identidade no IdP, com retentativa. Resta apenas um identificador anônimo, para impedir reaproveitamento de token antigo.
Onde estão os segredos hoje?
Em arquivos .env no servidor, com permissão restrita. Centralizar em um gerenciador é decisão tomada e ainda não implementada (ADR-007).
Alinhamento regulatório
LGPD: base legal declarada por finalidade, direitos do titular exercíveis dentro do app (exportação e exclusão) e canal do encarregado publicado. Não existe certificação SOC 2 nem auditoria externa — este é o produto de um desenvolvedor individual, e dizer o contrário seria falso.
Documento do titular

A base legal de cada finalidade, o prazo de retenção, a lista de operadores e o canal do encarregado estão na Política de Privacidade, escrita no mesmo nível de detalhe desta página.

[07] Desafios

Problemas reais, soluções medidas

Performance

A Home somava tudo em memória

Totais por período eram calculados carregando entidades, com N+1 por causa do OSIV.

Solução

Agregados em SQL, índice composto, OSIV desligado e um endpoint de resumo com read model mensal. O k6 define a meta: p95 < 300 ms com erros abaixo de 0,5%.

Estado

Compra de ativo antes do catálogo existir

Validar o ticker de forma síncrona falhava para moedas novas.

Solução

Rascunhos com retentativa manual, automática e diária (cron de segurança). Ao resolver, o Core publica asset.draft.resolved pelo outbox, e o Telegram avisa o usuário.

Dados

Histórico limitado pelo plano da BRAPI

O plano gratuito permite 15 mil requisições por mês, um ticker por chamada e 3 meses de candles diários.

Solução

Os candles se acumulam no Core, com orçamento diário de histórico configurável por ambiente. A busca é assíncrona por eventos, e a UI recebe atualizações via SSE por ticker.

Integração

Paginação com dois padrões

Transações e parties paginam a partir de 1; metas, a partir de 0. O bot erraria a primeira página.

Solução

O cliente do Telegram encapsula a diferença por endpoint, documentada e coberta por teste, em vez de mudar o contrato do Core às pressas.

UI / UX

Registrar em menos de três toques

Gastos viram hábito só se o registro for quase sem fricção.

Solução

O cinto de categorias do app (slots de um toque) e o bot do Telegram com teclado de respostas e máquina de estados. Gráficos de ativos em Skia, com gestos e animação na thread de UI.

[08] Fluxo interativo

Vinculando o Telegram, passo a passo

O fluxo de device authorization na prática — a implementação da ADR-003. Escolha um passo (ou use as setas do teclado) para ver quem faz o quê.

FLUXO 01DEVICE AUTHORIZATION GRANT · /VINCULAR
USUÁRIO → BOT

/vincular

O usuário pede o vínculo no chat. O bot inicia o device flow no Zitadel com o cliente dedicado ao Telegram.

POST /oauth/v2/device_authorization
client_id=vezta-telegram  scope=openid offline_access

[09] Entrega e DX

Disciplina, e o que dá para auditar

Trabalhar sozinho em cinco repositórios remove o revisor par. O que substitui ele é processo: contrato antes do código, fronteira que quebra o build e esquema versionado.

Execução local

README por repositório com os passos para subir o serviço e suas dependências, para que o projeto rode sem configuração adivinhada.

Em uso

Integração contínua

Pipeline por repositório, executada a cada push: build, compilação verificada e testes antes do merge no branch principal.

Em uso

Contrato antes do código

Uma issue-contrato por funcionalidade define endpoint, payload e evento antes da implementação — é o que substitui o code review quando se trabalha sozinho em cinco repositórios.

Em uso

Fronteira verificada

O Spring Modulith transforma dependência cíclica entre módulos em falha de build, não em descoberta tardia.

Em uso

Migrações versionadas

Flyway em cada serviço, com esquema evoluído por arquivo versionado e nenhuma alteração manual em produção.

Em uso

Ordem de entrega

Entrega faseada: domínio, depois identidade, depois o canal do Telegram, depois a coleta de cotações. Cada fase fechou antes de a seguinte começar.

Em uso

Testes ponta a ponta

Scripts E2E contra a stack local completa cobrem onboarding de perfil, exclusão de conta e vínculo do Telegram.

Em uso

Teste de carga

Cenários k6 com usuários de máquina do Zitadel: p95 < 300 ms, erros < 0,5%, queda de réplica e rampa de 0 a 10 mil. Escritos, aguardando ambiente.

Meta definida
VERIFICABILIDADECÓDIGO PRIVADO · ACESSO SOB PEDIDO

Os cinco repositórios são privados porque o produto está em pré-lançamento e a configuração de infraestrutura vive perto do código. Isso cria um problema legítimo para quem avalia: nada aqui é verificável só lendo a página. Como resolvo isso:

  • App funcionando — a prova mais direta é o próprio aplicativo publicado na Google Play, com as duas rotas de registro (app e bot) ativas.
  • Acesso de leitura sob pedido — concedo acesso de leitura aos repositórios a avaliadores identificados, para inspeção de README, histórico de commits, migrações, pipeline de CI e testes.
  • Walkthrough ao vivo — prefiro, inclusive, percorrer o código em chamada e responder por qualquer decisão desta página: o raciocínio é o que está em exame, não a sintaxe.

Para solicitar: [email protected]

[10] Post-mortem

O que aprendi, e o que ficou devendo

Nenhuma arquitetura sai certa. A parte útil de um projeto é a lista do que foi avaliado errado e da dívida que foi assumida sabendo o preço — com a condição que faz cada uma deixar de caber.

O que funcionou

  • Decidir identidade e estratégia de escala antes do lançamento, com zero usuário: custou ADRs em vez de migração em produção.
  • Eventos e outbox deixaram os scrapers e o bot evoluírem sem tocar no domínio.
  • Contrato escrito antes do código reduziu retrabalho entre os cinco repositórios.
  • Registrar a decisão substituída (ADR-000) em vez de apagá-la: a trilha de por que a auth própria caiu é o documento mais útil do conjunto.

Erros de avaliação inicial

  • Comecei pela arquitetura quando o gargalo estava nas queries. O teste de carga deveria ter vindo antes da primeira discussão de topologia.
  • Escrevi autenticação própria para economizar um componente, e paguei a migração da chave de identidade de e-mail para sub depois.
  • Deixei a paginação divergir entre endpoints (0 e 1) e só percebi quando o bot errou a primeira página — hoje o custo é um adaptador que não deveria existir.
  • Subestimei o efeito do onboarding just-in-time: cada tela passou a ter um estado de usuário autenticado sem perfil, e isso apareceu tarde no cliente.

Dívida assumida de propósito

  • Escala só vertical no Core. Cabe na meta de um nó; deixa de caber no dia em que a leitura saturar o hardware, e aí o caminho é extrair um módulo.
  • Consistência eventual no preço dos ativos, mitigada exibindo a hora da última atualização em vez de esconder o atraso.
  • Segredos em .env no servidor, com a substituição já decidida e não feita (ADR-007).
  • Sem cliente web, porque os gráficos em Skia não rodam no navegador — a decisão de stack do cliente fechou essa porta.
  • Os números de carga ainda são meta, não medição. Enquanto o k6 não rodar em ambiente próximo de produção, nada nesta página deve ser lido como resultado medido.

Converse sobre as decisões

O app está disponível para Android. Qualquer decisão desta página pode ser questionada diretamente com quem a tomou — inclusive as que estão marcadas como erro.