andré santos

projeto

Bolão da Copa do Mundo 2026 para grupos de amigos

App onde grupos de amigos palpitam os placares da Copa, com palpite que trava no início do jogo e resultados que entram sozinhos.

Next.jsTypeScriptSupabasePostgreSQLTailwind CSSGoogle OAuthGitHub ActionsPWA

contexto

Bolão de Copa entre amigos quase sempre vive em dois lugares: um grupo de WhatsApp e uma planilha que alguém mantém na mão. Funciona, mas tem os problemas de sempre. Alguém precisa digitar cada resultado e refazer a soma do ranking. Os palpites ficam à vista de todos, então dá pra copiar. E sempre tem a discussão de quem mandou o placar antes ou depois de o jogo começar.

Quis resolver isso com um app que se vira sozinho, e usei como projeto pra construir algo de ponta a ponta. Duas restrições guiaram as decisões: rodar de graça, sem API paga nem servidor próprio, e ter um login que amigo não abandona na metade. Senha e cadastro afastam; entrar com o Google é um toque.

A ideia central é simples: cada um cria ou entra num grupo por link, palpita os placares, e o ranking se atualiza sozinho conforme os jogos acontecem.

demo: "https://bolao-da-copa-tau.vercel.app/" repo: "https://github.com/andreldss/bolao-da-copa"

o que foi construído

Grupos

  • Criação de grupo por qualquer usuário, com link de convite
  • Entrada pelo link ou digitando o código na home
  • Grupo aberto ou fechado, controlado pelo dono
  • Sair do grupo (quem não é dono) e apagar o grupo (dono)
  • Limite de cinco grupos por pessoa

Palpites

  • Um palpite de placar por pessoa, por jogo, dentro de cada grupo
  • Trava no horário de início da partida
  • Palpites dos outros visíveis só depois que o jogo começa
  • Copiar todos os palpites em aberto de um grupo pra outro de uma vez

Pontuação e ranking

  • Placar exato vale 3 pontos, acertar só o resultado vale 1
  • Ranking por grupo, calculado a partir dos jogos encerrados
  • Lista de participantes e ranking na própria página do grupo

Jogos e resultados

  • Jogos importados de uma fonte pública (openfootball), sem chave de API
  • Resultados atualizados automaticamente por um job agendado
  • Jogos separados entre abertos e encerrados, com placar oficial

Acesso

  • Login com Google, sessão em cookie HTTP-only
  • Instalável no celular como PWA

decisões técnicas

Servidor como único acesso aos dados de grupo

As tabelas de grupo, membros e palpites não são lidas nem escritas direto pelo navegador. Todo acesso passa por rotas de API que checam quem é o usuário antes de qualquer operação. Cheguei a tentar resolver com RLS no Postgres, mas regra de "é membro do grupo" se referencia e cai em recursão. Centralizar a checagem no servidor é mais simples de ler e não tem esse problema. A interface esconde o que o usuário não pode fazer, mas a decisão fica no backend.

Trava do palpite no servidor

O palpite só é aceito se o jogo ainda não começou, e essa verificação roda no servidor, comparando o horário da partida com a hora atual. A interface também esconde o campo depois do início, mas isso é só conveniência. A trava de verdade é a do backend, porque o que está no navegador dá pra burlar.

Palpite dos outros escondido até o jogo começar

A regra de leitura devolve o palpite de outra pessoa só se a partida daquele palpite já começou. Antes disso, cada um enxerga apenas o próprio. Sem isso, bastava abrir a tela pra ver o que os outros chutaram e copiar. A justiça do bolão depende dessa regra estar no servidor, não na tela.

Resultados por job agendado, sem API paga

Os jogos e placares vêm do worldcup.json do openfootball, que é aberto. Uma rota baixa esse arquivo e grava ou atualiza os jogos. Pra não rodar isso na mão, um GitHub Action chama essa rota a cada 30 minutos, autenticado por um segredo compartilhado. Usei GitHub Actions porque o cron grátis da Vercel roda só uma vez por dia, pouco pra um dia com vários jogos. Ninguém digita resultado.

Importação idempotente por chave estável

Cada jogo tem um identificador estável, e a importação faz upsert por essa chave. Rodar de novo nunca duplica, só atualiza o que mudou (o placar, ou o time definido depois da fase de grupos). Sem uma chave estável, reimportar criaria jogos repetidos.

Login com Google em cookie HTTP-only

A sessão fica num cookie HTTP-only, renovada no middleware a cada requisição, e não toca o JavaScript do frontend. Escolhi Google em vez de e-mail e senha porque o público é um grupo de amigos, e cadastro com senha é onde a pessoa desiste.

capturas de tela

Home com os grupos do usuário e o formulário de criar ou entrar

Home com os bolões do usuário e o formulário de criar ou entrar por código

Tela de palpites com os jogos abertos

Tela de palpites, com a trava por horário, abas de jogos abertos/encerrados e opção de copiar palpites abertos de outro grupo

Ranking do grupo

Participantes do grupo

o que aprendi

As duas regras que fazem o bolão ser justo, travar o palpite no horário e esconder o palpite dos outros, só são reais porque estão no servidor. A versão delas na interface é só conforto visual. Qualquer coisa que dependa de integridade não pode morar no navegador.

Restrição de custo virou decisão de projeto, não obstáculo. O cron grátis da Vercel rodar uma vez por dia me levou ao GitHub Action; não ter orçamento pra API de futebol me levou ao openfootball. As duas escolhas acabaram simples e suficientes.

O que mais me custou tempo não foi nenhuma feature, foi um service worker antigo servindo conteúdo velho do cache. Cache é fácil de ligar e difícil de depurar. Da próxima vez penso duas vezes antes de cachear, e registro o service worker só onde ele precisa existir.

Bolão da Copa do Mundo 2026 para grupos de amigos - André Luiz