01 Visão geral
Um app de treino construído para resolver as reclamações reais de um app de treino
Eduardo é personal trainer em Divinópolis/MG, com atuação presencial e online, especialização em Musculação e Condicionamento Físico e mais de 5 anos de experiência. Antes do projeto, ele — como a maioria dos personal trainers brasileiros — dependia de um aplicativo de terceiros (MFit) para prescrever e acompanhar os treinos dos alunos. O ponto de partida do App Personalizado - Personal Trainer (nome interno do projeto) não foi uma lista de funcionalidades desejadas, e sim uma lista de queixas reais sobre a ferramenta anterior: sessão caindo sem aviso, "acesso não autorizado" aparecendo do nada, e nenhum autoatendimento para o aluno resolver o próprio cadastro.
O resultado é uma plataforma full-stack com dois portais completamente separados — um para o personal, outro para o aluno — construída sobre Next.js 16 (App Router) no frontend e Supabase (Postgres + Auth + Storage) no backend, hospedada inteira no Cloudflare Workers. Sessão via JWT com refresh automático resolve diretamente o problema de autenticação instável; toda regra de acesso é reforçada no banco via Row Level Security, nunca só no código do frontend.
O projeto foi especificado antes de qualquer linha de código num documento de arquitetura próprio (PROJECT.md, ~500 linhas) cobrindo modelo de dados, segurança, LGPD e um roadmap em etapas — e vem sendo construído seguindo esse roadmap uma parte por vez, com validação do Eduardo a cada etapa entregue.
02 Stack técnica
Next.js 16 + Supabase, sem servidor próprio para manter
Toda peça da stack roda gerenciada — nenhum servidor, fila ou banco para operar manualmente. O app inteiro (SSR incluso) é compilado para rodar como Cloudflare Worker via adaptador OpenNext.
16.2
19.2
CSS v4
@theme inlineWorkers
DeepSeek
03 A técnica central
Uma biblioteca de exercícios em inglês, traduzida e mantida em pt-BR sem trabalho manual
Em vez de digitar manualmente uma biblioteca de exercícios, o app consome e sincroniza o catálogo completo do ExerciseDB (via RapidAPI) e executa um pipeline interno de localização e tradução técnica automática para português do Brasil via DeepSeek. Diretrizes estritas de parsing e sanitização de dados garantem fidelidade total das instruções originais, sem intervenção humana manual em nenhuma etapa.
O pipeline roda sob demanda, disparado pelo próprio Eduardo na tela de exercícios, e foi desenhado para nunca travar a interface nem estourar cota de API por acidente: uma checagem barata de "tem exercício novo?" (uma única requisição, cacheada por 1h) evita rodar a sincronização completa sem necessidade, termos de categoria repetidos (grupo muscular, equipamento) são traduzidos uma única vez e reaproveitados, e toda chamada a uma API externa tem retry com backoff exponencial respeitando o cabeçalho Retry-After.
página 1
completo
Regras do pipeline
- Paginação por cursor, com rede de segurança. A API do ExerciseDB ignora silenciosamente o parâmetro
offset— descoberto testando ao vivo (offset=0eoffset=25devolviam a mesma página). A paginação real usameta.nextCursor; mesmo assim, um teto absoluto de 200 páginas impede loop infinito se a API voltar a se comportar mal. - Tradução com validação por campo. Se a resposta da IA vier com uma lista de instruções de tamanho diferente do original, só aquele campo específico cai de volta para o texto em inglês — a tradução dos demais campos não é descartada.
- JSON estrito, sem markdown. O prompt exige um objeto JSON puro; a rotina de parse tenta o JSON direto e, se falhar, extrai o primeiro bloco
{...}da resposta como rede de segurança adicional. - Falha nunca é silenciosa a ponto de mascarar dado ruim. Erros de rede (429/5xx) acionam retry; erros de estrutura (JSON inválido, contagem de itens errada) acionam fallback controlado — nunca um dado inventado é salvo como se fosse tradução real.
04 Arquitetura de segurança e dados
O frontend nunca decide o que o usuário pode ver
A regra de ouro definida antes da primeira linha de código: o frontend nunca decide o que um usuário pode ver ou alterar — isso é sempre reforçado no banco, via Row Level Security. O frontend só reflete o que o Postgres já permitiu.
Regras de segurança aplicadas
- RLS em 100% das tabelas. 21 tabelas, 60 policies — inclusive tabelas que "parecem" só de leitura, como a biblioteca global de exercícios.
- Chaves sensíveis isoladas do client.
SUPABASE_SERVICE_ROLE_KEY,RESEND_API_KEY,RAPIDAPI_KEYeDEEPSEEK_API_KEYsó existem em código de servidor (rotas marcadasserver-only), nunca em variável pública. - Mídia sempre privada. Buckets de vídeo de exercício e foto de avaliação são privados por padrão — nenhuma URL pública permanente, apenas signed URLs geradas sob demanda.
- Log de auditoria. Tabela
audit_logregistra ações sensíveis (exclusão de aluno, alteração de vínculo) para permitir investigar reincidência de bug ou uso indevido depois do fato. - Provisionamento controlado do primeiro acesso. Cadastro público foi substituído por um fluxo de convite administrado pelo trainer — o aluno só entra no sistema depois de vinculado por Eduardo, nunca por auto-cadastro aberto.
05 Sistema de marca em código
O mesmo manual de identidade, com uma regra a mais: dois portais, um símbolo só
A paleta e a tipografia seguem o mesmo manual de identidade oficial do Eduardo usado no site institucional — mas o app precisava resolver um problema que o site não tinha: diferenciar visualmente dois portais (personal e aluno) sem parecer dois produtos diferentes.
Identidade de portal (Manual, item 2.10): "mesmo símbolo, muda só o acento" — trainer e aluno usam o mesmo logotipo, a mesma tipografia (Montserrat display + Inter corpo) e o mesmo raio de canto de 3px; o que muda entre um portal e outro é só a cor de destaque (--em-portal-trainer vs --em-portal-student), resolvida em uma única linha de CSS por contexto.
Um detalhe de rigor: cor de marca nunca é cor de dado
O manual proíbe explicitamente usar bronze ou teal "puros" como cor de texto de um dado numérico — carga, séries, repetições. A regra está documentada como comentário no topo do próprio arquivo de tokens do projeto, não só num PDF de marca separado: --em-bronze e --em-teal só podem aparecer em borda, ícone e rótulo; para texto legível, o token mapeado é sempre uma variante já calibrada para contraste (--em-bronze-light/--em-bronze-dark, --em-teal-light/--em-teal-dark), escolhida automaticamente conforme o app está em modo escuro ou claro.
O modo claro, aliás, não é tratado como modo "alternativo": o próprio manual da marca declara que "o modo claro é oficial, não uma exceção" — por isso o app implementa os dois temas com o mesmo nível de acabamento, com persistência da escolha do usuário e sem depender só da preferência do sistema operacional.
06 Funcionalidades
Dois portais, um banco de dados só
Dashboard por ordem de urgência
Painel do personal reorganizado para mostrar primeiro o que precisa de ação — semáforo de risco por aluno, métricas reais de adesão, feedback recente e ranking de frequência — em vez de uma lista neutra de alunos.
Esteira inteligente de recuperação
Algoritmo dedicado que monitora padrões de frequência — alunos inativos há mais de 7 dias ou com anamnese pendente — e executa ações automáticas de retenção e engajamento, com trava anti-spam diária antes de qualquer disparo.
Biblioteca de exercícios própria
Catálogo global sincronizado e traduzido (Seção 03), mais exercícios customizados do próprio Eduardo, com upload de vídeo e edição manual de qualquer campo traduzido.
Criação e atribuição de treino
Rotinas com séries/reps/carga/descanso por exercício, prescrição de carga guiada pelo objetivo declarado na anamnese do aluno, atribuídas a um ou mais alunos.
Anamnese de 17 perguntas com trava de acesso
Questionário de saúde e objetivos que bloqueia o acesso ao treino até ser preenchido, com modal de boas-vindas e checkbox de legitimidade das respostas.
Avaliação física + evolução
Registro simplificado (peso, %gordura, massa magra, circunferências, fonte) com gráfico de evolução comparando as três métricas ao longo do tempo.
Execução de treino e feedback
Tela "treino de hoje", sessão com marcação de série/carga em tempo real e feedback pós-treino (nota + comentário) que alimenta o dashboard do trainer.
Mensagens e notificações in-app
Conversa direta entre trainer e aluno por vínculo, com notificações in-app para treino atribuído, feedback recebido, vínculo aceito e mensagem nova.
07 Métricas de engenharia & resiliência
Escala do sistema até este relatório
Sem ferramenta de auditoria externa envolvida neste relatório — os números abaixo vêm direto do repositório: schema, histórico de commits e código de aplicação.
supabase/migrationsDecisões de resiliência que aparecem no código, não só no discurso
- Retry com backoff exponencial em toda chamada externa. ExerciseDB e DeepSeek têm o mesmo padrão de retry: até 4 tentativas, respeitando
Retry-Afterquando presente, com espera crescente caso contrário. - Teto de segurança independente do que a API promete. A sincronização de exercícios para em no máximo 200 páginas mesmo se os campos
meta.total/hasNextPageda API voltarem a mentir — não depende de a API terceira estar correta para não travar. - Checagem barata antes da cara. "Tem exercício novo na biblioteca?" é uma única requisição cacheada por 1h (
revalidate: 3600), separada da sincronização completa — evita gastar cota de API só porque alguém recarregou a página de exercícios. - Falha de notificação nunca derruba a ação principal. Criar uma notificação in-app é best-effort: se falhar, loga e segue — um aluno concluir o treino nunca pode falhar por causa de uma notificação que não foi enviada.
08 Engenharia & depuração
Quatro bugs que só aparecem em produção
Registro honesto de problemas reais encontrados durante o desenvolvimento — o tipo de detalhe que separa "funciona no meu navegador" de "funciona".
O mesmo bug de navegação, três vezes
router.push() seguido imediatamente de router.refresh() no mesmo handler faz o refresh tentar revalidar os dados da página que está sendo deixada enquanto a navegação do push ainda está em andamento — os dois competem e a transição nunca resolve, deixando o botão preso em "Enviando..." para sempre, mesmo com o dado já salvo com sucesso no servidor. Encontrado ao vivo no feedback pós-treino; a causa raiz só ficou clara reproduzindo o fluxo passo a passo.
Correção: remover o refresh() redundante — o push() já busca dados novos da rota de destino sozinho. O mesmo padrão reapareceu no envio da anamnese e no botão "Entrar" do login dias depois; encontrado em minutos ao grepar todo router.push() seguido de router.refresh() no restante do código, já com o padrão mapeado.
A paginação que sempre devolvia a primeira página
A sincronização da biblioteca de exercícios usava o parâmetro offset documentado pela API do ExerciseDB para avançar de página — mas a API o ignora silenciosamente. offset=0 e offset=25 devolviam exatamente a mesma resposta, então o loop de sincronização lia a página 1 para sempre, achando (por contagem de itens recebidos) que estava avançando.
Correção: paginação real é por cursor (meta.nextCursor, passado como after=<id>), com deduplicação por ID e um teto absoluto de 200 páginas como rede de segurança contra qualquer novo comportamento inesperado da API.
Variáveis de ambiente somem sozinhas a cada deploy
Sem uma configuração de ambiente devidamente versionada no repositório, o pipeline de build da Cloudflare Workers reexecutava automaticamente uma etapa de migração a cada novo deploy — regenerando a configuração de runtime do zero, sem conhecimento das variáveis previamente cadastradas no painel. O resultado prático era o sintoma clássico de "as credenciais somem sozinhas" a cada nova publicação.
Ajuste: reestruturação da arquitetura de ambientes e sincronização de variáveis de runtime no servidor de borda (Edge), garantindo persistência de credenciais sem dependência do ciclo de build.
Uma incompatibilidade silenciosa de runtime entre framework e adaptador de borda
Uma peça central do roteamento do Next.js 16 é projetada, por padrão, para um runtime que o adaptador de borda da Cloudflare ainda não suporta totalmente — gerando falha direta no deploy em produção. A causa raiz só ficou clara depois de testes comparativos diretos entre diferentes convenções de bundle contra o ambiente real de deploy.
Ajuste: adaptação do bundle de borda para manter total compatibilidade com os seletores de runtime do Next.js 16 e do adaptador Edge, garantindo o processamento de rotas em baixa latência no nível da CDN.
09 Conformidade & responsabilidade
Dado de saúde é tratado como dado de saúde, não como campo de formulário
A anamnese pergunta explicitamente sobre limitação médica, cirurgia prévia, medicamento de uso contínuo e grupo de risco (pressão, diabetes, doença coronária) — informação que existe para o Eduardo adaptar a prescrição de treino dentro do escopo de um profissional de Educação Física registrado no CREF, nunca em substituição a acompanhamento médico. A avaliação física foi mantida deliberadamente simples (peso, %gordura, massa magra, fonte, data) em vez de implementar protocolos próprios de bioimpedância — quanto menos dado sensível de saúde armazenado além do estritamente necessário, menor o risco.
- LGPD. Anamnese e avaliação física são tratadas como categoria sensível; consentimento explícito é coletado via checkbox de legitimidade das respostas, com modal de boas-vindas bloqueante antes do primeiro preenchimento.
- Segurança por padrão, não por confiança. "O frontend nunca decide o que o usuário pode ver ou alterar" é a regra documentada desde a especificação inicial do projeto (Seção 04) — reforçada no banco, não pela boa vontade do código de tela.
- Segredos fora do repositório. Todas as chaves de API (Supabase service role, Resend, RapidAPI, DeepSeek) vivem só em variáveis de ambiente de servidor —
.env.localno.gitignore,.env.exampleversionado sem nenhum valor real. - Provisionamento administrado. O aluno nunca se auto-cadastra livremente: o vínculo é criado pelo trainer, o primeiro acesso é validado por convite — reduzindo superfície para conta fraudulenta ou dado de terceiro entrando no sistema sem vínculo real com o Eduardo.
10 Deploy
Do commit ao ar, num único worker
Deploy serverless de alta performance: a aplicação inteira — incluindo Server-Side Rendering (SSR), Server Actions e ativos estáticos — é compilada e implantada na rede global da Cloudflare Workers via adaptador de borda (Edge). A arquitetura dispensa gestão de servidores tradicionais e mantém latência mínima em qualquer região do mundo, sem pipeline própria de CI a manter.
Variáveis de ambiente de produção são cadastradas diretamente no painel da Cloudflare, nunca no repositório, com persistência garantida entre deploys (Seção 08, item 03). As migrations do banco de dados são versionadas e aplicadas em um pipeline isolado e seguro, independente do fluxo de publicação do app — reduzindo o risco de inconsistência entre schema e aplicação em produção.