Trabalho

Produto próprio · 2026 · em produção

Kravva

Plataforma de palpites de futebol com rankings, bolões, loja com PIX e placar ao vivo. Construo e opero sozinho, do produto à infraestrutura.

Papel
Fundador, produto e engenharia
Stack
Vue 3, Quasar, NestJS, PostgreSQL, Redis, BullMQ, Socket.io, Docker, GitHub Actions
  • 16filas BullMQ
  • 31módulos no back-end
  • 96migrations
  • 2ambientes: produção e homologação

O problema

Queria um produto meu, com back-end de verdade. O Kravva é uma plataforma de palpites de futebol: o usuário palpita placares, entra em bolões, disputa rankings, acompanha jogos ao vivo e compra passes via PIX. Tem loja, afiliados e uma camada social.

Isso parece simples até o jogo começar. O placar muda e todo mundo quer ver na hora. A pontuação precisa ser recalculada no apito final. A API de dados esportivos tem cota diária. E o dinheiro entra por webhook, que o provedor reenvia quando quer.

Eu era a única pessoa para resolver tudo isso, do produto ao deploy.

O que eu fiz

Back-end modular

A API é um monólito modular em NestJS 11 com PostgreSQL 16: 31 módulos, 82 entidades e 96 migrations. As migrations são SQL escrito à mão e a CI aplica todas num Postgres efêmero antes de qualquer deploy. Auth com JWT e refresh token, SSO Google e RBAC.

Filas para tudo que não cabe numa requisição

São 16 filas BullMQ: sincronização de partidas ao vivo, pontuação, notificações, e-mail e retenção de dados para a LGPD. Os jobs usam IDs determinísticos. Assim, o mesmo gol ou o mesmo apito final não é processado duas vezes.

Tempo real em cluster

A API roda em cluster Node, com mais de um worker. O Socket.io usa Redis adapter e autenticação JWT no handshake. Quando uma compra é confirmada ou o placar muda, o evento sai do worker que processou e chega ao socket do usuário, esteja ele em qual worker estiver.

kravva · placar ao vivo em cluster
partida1 × 0
BullMQfila de sync
worker 1
1×01×0
worker 2
1×01×0
Redis adapter
todos os clientes sincronizados
Simplificado. Cada worker do cluster tem os próprios sockets; sem o Redis adapter, um emit não atravessa para o outro worker.

PIX com webhook e ledger

O webhook chega com assinatura HMAC sobre o corpo cru, que valido com comparação em tempo constante. A confirmação roda numa transação com lock pessimista na linha do pagamento e grava num ledger financeiro append-only. Estorno é lançamento novo, nunca update.

// Confirmação idempotente (pseudocódigo)
await db.transaction(async (tx) => {
  const pagamento = await tx.selectForUpdate(pagamentos, { id }); // trava a linha
  if (pagamento.status === 'pago') return; // reentrega do webhook: nada a fazer

  pagamento.status = 'pago';
  await tx.save(pagamento);
  await ledger.registrarSeAusente(tx, pagamento); // lançamento append-only
  await acessos.concederSeAusente(tx, pagamento);
  await comissao.creditar(tx, pagamento); // só na primeira confirmação
});
// O evento de tempo real só sai depois do commit
emitirParaUsuario(pagamento.usuarioId, 'acesso_liberado');

Se o webhook atrasa, o front consulta a transação. Depois de um tempo pendente, a API pergunta ao provedor e chama o mesmo caminho idempotente.

Operação

Rotinas em background precisam rodar em um único processo, então elegi um líder por lease no PostgreSQL. O deploy é automatizado com GitHub Actions e SSH numa VPS com Docker e Nginx. O front fica no Cloudflare Pages e os arquivos no R2. Há ambiente de produção e de homologação.

Decisões e trade-offs

Idempotência em camadas no pagamento. Histórico de eventos, SELECT ... FOR UPDATE, checagem de estado e índices únicos como última barreira. Escolhi lock pessimista porque provedores de pagamento reenviam em rajada e a seção crítica é curta. O custo: entregas concorrentes do mesmo pagamento esperam umas pelas outras.

BullMQ com jobId determinístico. A fila garante entrega pelo menos uma vez, então o processamento precisa tolerar repetição. O ID do job impede duplicatas. O custo: a deduplicação só vale enquanto o job está retido. Já tive de pensar em incluir o placar no ID para que uma correção de VAR gerasse um job novo.

SQL à mão nas migrations. Dá controle sobre índices parciais e backfills, que o gerador do TypeORM faz mal. O custo: nada garante que a entidade continue igual à coluna real. Isso depende de disciplina e de teste.

Cluster em vez de orquestrador. Dois workers aproveitam a máquina sem a complexidade de Kubernetes. O custo é visível no próximo ponto: estado em memória por processo.

Resultado

O Kravva está em produção, com deploy automatizado, testes unitários com Jest, testes de integração com Postgres real e CI que faz dry-run das migrations. Não divulgo o número de usuários.

Três incidentes reais viraram correção:

  • WebSocket caindo no cluster. O front recebia "Session ID unknown" (HTTP 400). A causa: o Nginx não repassava o upgrade de WebSocket, o cliente caía para long polling e as requisições do mesmo cliente chegavam a workers diferentes. Adicionei o bloco do Socket.io no Nginx com Upgrade, e o erro sumiu. Foi um problema separado do Redis adapter, que adicionei no mesmo ajuste para resolver uma falha silenciosa: um evento emitido num worker não chegava a sockets conectados em outro.
  • Cota da API de futebol estourada na Copa. Passei a usar um snapshot ao vivo compartilhado entre workers e um rate limiter global no Redis.
  • PWA servindo arquivos velhos após deploy. Tratei a recuperação de chunk e o skipWaiting.

Sobre IA: boa parte do código foi escrita com apoio de Cursor e Claude. As decisões, a revisão e a operação são minhas, e sei explicar cada uma delas.

O que eu faria diferente

Começaria com observabilidade: logs estruturados, métricas das filas e alertas. Essa é uma lacuna que conheço.

Quebraria o serviço de sincronização de jogos, que cresceu demais.

Fecharia o fallback de long polling com WebSocket puro ou sticky sessions. Sem isso, o polling continua frágil com mais de um worker.

Testaria as migrations contra um dump anonimizado, não só contra um banco vazio. E padronizaria o acesso a dados, que hoje mistura repositórios próprios com o repositório padrão do TypeORM.

Maycon Liborio

São José dos Campos, SP

Aceito projetos freelance pontuais.

© 2026 · Feito à mão com Nuxt, CSS moderno e zero bibliotecas de UI. Dados gerados do meu perfil-mestre a cada build.

esc

Navegar

Projetos

Ações

↑↓ navegar↵ abrir/ ou ⌘K alterna