acoelho.dev
Portfolio Website
Categoria:
Aplicação Web Full-Stack
Tech Stack:
Escopo de IA:
Revisão de código e suporte estrutural. As decisões de arquitetura foram minhas.
Índice:
>API
Um site de portfólio desenvolvido como um produto real, com suporte a localização, testes, CI/CD, hardening de segurança e um deploy reproduzível que tenho total controle. Criado para demonstrar minha capacidade técnica de entregar projetos em nível de produção e meu interesse em aplicações gamificadas que geram métricas de engajamento. O site que você está visitando é o próprio projeto.
Arquitetura
Cada camada foi desenvolvida para ser reproduzível, algo que posso reconstruir do zero quando necessário. O ambiente de produção é atualizado com um único push para main. O GitHub Actions executa a suíte de testes, constrói a imagem a partir de um Dockerfile multiestágio e provisiona o host com um arquivo docker-compose.
A posição de cada camada no stack foi uma escolha deliberada, resultado de várias etapas de evolução até chegar à forma atual (detalhadas na seção 4). A seguir, os fundamentos por trás de cada uma:
Camada de CI/CD — GitHub
GitHub push → Actions (CI: scan + test) → Actions (CD: SSH deploy)
A suíte cobre roteamento, renderização de páginas, o fluxo de coleta de moedas e a lógica de modelos com banco de dados. Escrita deliberadamente para que uma build quebrada não chegue à produção.
Camada de Servidor — Hostinger VPS & Docker
Caddy (TLS) → Docker Compose → web (Puma) + db (Postgres)
Um único VPS da Hostinger se mostrou o equilíbrio certo entre custo e simplicidade operacional para um projeto deste tamanho. Esta camada passou pela maior evolução — veja mais detalhes na seção 4.
Camada de App — Ruby on Rails
i18n Locales → JS de Gamificação ↔ Event API → Cache de Métricas ↔ PostgreSQL
O projeto começou como HTML e JavaScript simples, mas o Rails foi a ferramenta certa à medida que cresceu: um monolito robusto e enxuto que consolidou todos os serviços sem a complexidade de micro-serviços externos que o projeto não precisava.
Camada de Conteúdo — Rails i18n
Suportar múltiplos idiomas era essencial para este portfólio. O i18n nativo do Rails tornou isso direto: rotas por idioma, textos entregues por arquivos YAML e a experiência completa servida em inglês e português.
Camada de Client — JS de Gamificação
Um jogo interativo onde os visitantes coletam três moedas escondidas, acompanham seu próprio progresso e veem o total acumulado por todos os outros visitantes. É uma demonstração simples e direta de como um design gamificado pode gerar engajamento, e um reflexo do meu interesse em construir sistemas gamificados.
Camada de Dados — PostgreSQL
O progresso de moedas de cada visitante fica armazenado no localStorage do navegador, enquanto um registro anônimo de eventos no Postgres agrega as coletas de todos os visitantes, criando um senso de comunidade sem armazenar nenhuma informação pessoal.
Conceitos
Este projeto foi criado com a intenção de demonstrar três conceitos principais:
- Um portfólio vivo.
Ambiente de produção auto-hospedado, facilmente reproduzível e totalmente sob meu controle, atualizado com um único git push.
- Disciplina de produção.
Docker multiestágio, pipeline de deploy via SSH, cache de API de 12 horas, varreduras de segurança no CI, uma suíte de testes real, CSP e rate limiting. Gestão de projeto e atenção aos detalhes que não são necessários para um portfólio pessoal, mas que demonstram como eu trataria o sistema de qualquer cliente.
- Engajamento mensurável com gamificação.
Um jogo de moedas que alimenta uma API real, e uma página inicial que fecha a experiência com contadores globais ao vivo. Uma experiência gamificada que demonstra engajamento mensurável.
Estes conceitos foram os princípios que guiaram cada página e cada decisão de navegação. A experiência de gamificação foi projetada como ponto de entrada para conquistar a atenção do visitante antes de conduzi-lo pelo conteúdo do portfólio em direção ao contato. O fluxo abaixo representa a jornada do usuário que orientou cada decisão de navegação e layout antes de qualquer linha de código ser escrita.
Evolução
Durante o desenvolvimento deste projeto, a estrutura de back-end e a arquitetura de hospedagem evoluíram com diferentes etapas, cada uma como resposta a restrições reais e uma busca por reduzir complexidade e custo sem perder o controle técnico.
Fase 1 — GitHub Pages, sem back-end
GitHub Pages, HTML puro, Javascript, CSS
O objetivo era colocar o projeto no ar rapidamente usando apenas recursos gratuitos, sem backend e sem serviços externos. O GitHub Pages cuidou da hospedagem e foi o ponto de partida perfeito para o projeto.
Com a base estática no lugar, o próximo passo era dar ao loop de gamificação um passo final. Os visitantes podiam coletar moedas, mas sem um backend não havia como registrar as coletas ou exibir o progresso da comunidade.
Motivo da migração:
- O loop de gamificação não tinha um passo final. Os visitantes coletavam moedas mas não conseguiam ver o impacto na comunidade. Uma API real era necessária para fechar esse loop.
Fase 2 — AWS + GitHub Pages
AWS (EC2 + RDS), Node.js, Express.js, MySQL, HTML, Javascript, CSS
Para dar suporte à experiência de gamificação com dados reais, desenvolvi uma API Node.js no AWS EC2 e conectei a uma instância RDS MySQL. A AWS facilitou a configuração, mas para um projeto pessoal significava manter dois serviços gerenciados e absorver os custos de datacenters sul-americanos. Migrar para servidores AWS na América do Norte reduziria o valor, mas os dois serviços gerenciados e a cobrança variável por uso continuavam sendo riscos para o projeto.
Com a arquitetura no lugar, foquei em manter o projeto enxuto adicionando cache no servidor para reduzir as consultas ao RDS a cada carregamento de página, e unifiquei a invalidação do cache com a transação de registro de coleta de moedas para que o cache se mantivesse preciso sem uma etapa separada.
Decisões principais:
- Cache adicionado no servidor para reduzir o tráfego no RDS a cada visita à página.
- Invalidação de cache com a transação de escrita de moeda para precisão e eficiência.
Motivo da migração:
- Dois serviços gerenciados (EC2 + RDS) adicionaram complexidade operacional difícil de justificar para um projeto de desenvolvedor solo.
- A AWS custava cerca de $40 USD por mês via créditos do plano gratuito, sem um teto claro para os custos futuros à medida que o uso pudesse crescer. Migrar para servidores AWS na América do Norte teria resolvido parcialmente o custo, mas ainda deixaria dois serviços gerenciados e uma cobrança mensal variável. O plano de custo fixo de 2 anos da Hostinger por $30 BRL (~$6 USD) por mês, também hospedado na América do Norte, eliminou os dois problemas de uma só vez.
Fase 3 (atual) — Hostinger VPS + Rails
Hostinger, Docker, Ruby on Rails, PostgreSQL, Caddy
Refatorei o projeto em um monolito Rails e hospedei em um único VPS da Hostinger provisionado com Docker Compose. Tudo que antes estava dividido entre Github Pages, EC2 e RDS agora vive em um único ambiente reproduzível, com deploy automático a cada push para main.
O monolito eliminou a latência de chamadas a uma API externa e os custos de operação do EC2 + RDS. Também permitiu que o contador da comunidade fosse atualizado em tempo real a cada coleta de moeda, sem recarregamento de página e sem polling, algo que não era prático com uma API externa.
O Railway foi avaliado como alternativa, mas um VPS oferece visibilidade completa do ambiente sem abstrações que escondam a infraestrutura, e a flexibilidade de continuar expandindo com novos serviços no futuro.
Decisões principais:
- Consolidação da API EC2 + RDS em um único monolito Rails com um codebase, um servidor e custo fixo menor.
- Provisionamento com Docker Compose, totalmente reproduzível a partir do código-fonte, com deploy automático a cada push.
- Contador da comunidade em tempo real, atualizado a cada coleta, sem recarregamento de página ou polling.
Decisões
Algumas decisões neste projeto foram definidas desde o início, enquanto outras surgiram à medida que o projeto evoluiu. Ambas refletem como avaliei tradeoffs e tomei decisões técnicas durante esse processo.
Decisões que tomei deliberadamente:
i18n bilíngue com routes (scope '(:locale)', locale: /en|pt/) por locales para alcançar ambos públicos sem manter páginas ou codebases separados.
Cadeia de detecção de idioma (URL → cookie → cabeçalho Accept-Language no ApplicationController) para que os visitantes recebam o idioma correto automaticamente, sem necessidade de seleção manual.
Sistema de design CSS construído manualmente com design tokens.css e sem framework CSS, para ter controle total da linguagem visual e evitar a introdução de uma dependência de framework.
Experiência gamificada de coleta de moedas com três mini-games distintos: index.js iniciando a moeda escondida na caixa, a barra de preenchimento por momentum em fillBar.js, e pedra-papel-tesoura em rock-paper-scissors.js; com persistência em localStorage via gameState.js, para demonstrar como a gamificação pode gerar engajamento e refletir meu interesse genuíno em construir sistemas gamificados.
API de rastreamento de eventos POST /api/record-event registrando cada moeda por cliente anônimo para dar ao loop de gamificação um ponto final mensurável e conectar ações individuais a um total compartilhado pela comunidade.
Métricas ao vivo agregadas no servidor MetricsCalculator exibidas como contadores globais na página inicial para adicionar uma dimensão comunitária que dá significado compartilhado a cada coleta individual e motiva maior participação.
Cache Rails de 12 horas no MetricsCalculator#L3 para servir do cache e evitar recálculo dos mesmo valores a cada carregamento de página.
Invalidação de cache a cada nova coleta em MetricsCalculator#L8 atualizando as métricas na mesma transação da escrita de moeda para manter o contador preciso sem uma etapa separada.
Identidade anônima do cliente via cookie UUID httpOnly, SameSite=Lax, com validade de 1 ano EventsController#L48 para coletar dados de participação sem exigir contas, diálogos de consentimento ou qualquer informação pessoal do visitante.
Migração do AWS (EC2 + RDS) para um único host Docker por simplicidade de custo e operação.
Docker Compose docker-compose.yml em um único VPS para um ambiente consolidado e reproduzível, com deploy automático a cada push.
Suíte de testes CI no GitHub Actions (testes unitários + de sistema, Brakeman, bundler-audit, importmap audit, RuboCop) e deploy CD via SSH com retry .github/workflows/ para manter a disciplina de produção e garantir que uma build quebrada nunca chegue ao ambiente de produção.
Hardening de segurança: CSP, rate limiting Rack::Attack, CSRF, validação de moedas no modelo CoinEvent, suíte de testes completa; para evitar regressões e aplicar os mesmos padrões de segurança que usaria em qualquer projeto de cliente.
Padrões de Rails 8 adotados:
Puma como servidor de aplicação Rails e Caddy como proxy reverso, ambos boas opções para um deploy em VPS único.
Migração do MySQL (usado na Fase 2 pelos padrões do AWS RDS) para PostgreSQL na Fase 3, com suporte mais profundo ao Active Record e banco de dados preferido do Rails (config/database.yml).
A lógica de gamificação foi escrita antes da migração para Rails e mantida em vanilla JS. O Stimulus é projetado para interatividade leve em páginas renderizadas pelo servidor, não para game loops com estado, então a abordagem original continuou sendo a mais adequada.
Hardening do Docker em produção com usuário não-root, alocador de memória jemalloc e precompilação do bootsnap (Dockerfile) para um container mais leve e rápido e para manter o uso de recursos baixo em um VPS de custo fixo.
Solid Cache rodando no banco de dados para o cache de 12 horas do MetricsCalculator (production.rb) para evitar rodar uma instância Redis separada e manter toda a persistência no banco de dados existente.
Importmap-rails entregando ES modules (config/importmap.rb) para servir JavaScript sem uma etapa de build, mantendo o pipeline de deploy simples e removendo Node como dependência.
Manifesto PWA habilitado para UX mobile sem lógica offline adicional (application.html.erb#L19) para que visitantes no celular possam instalar o site como um app, caso queiram.
Gamificação
Gamificação é a aplicação de elementos de jogo para motivar o engajamento do usuário, uma área pela qual tenho forte interesse. O objetivo era demonstrar como mecânicas simples de jogo podem tornar um portfólio pessoal memorável, com toda a experiência projetada para se desenrolar em um único scroll da página inicial.
O diagrama acima descreve o fluxo de três etapas: descoberta ao longo do scroll da página, clareza de objetivo através do overlay, e a recompensa comunitária no final. Cada jogo foi escrito manualmente em vanilla JS como um módulo independente, sem nenhum framework de jogo. As seções abaixo descrevem como cada parte foi construída.
Elementos principais da experiência de jogo:
- Moeda Escondida index.js
Uma moeda intencionalmente escondida na página como primeiro desafio de descoberta. O overlay indica sua localização para que os visitantes possam voltar para coletá-la caso passem por ela.
- Encha a Barra fillBar.js
Uma barra baseada em momentum que é preenchida por pressões repetidas no botão e decai ao longo do tempo se deixada sozinha. Um cronômetro integrado registra a velocidade de conclusão, armazena o melhor tempo pessoal e incentiva tentativas repetidas para superá-lo.
- Pedra Papel Tesoura rock-paper-scissors.js
Um jogo de três opções onde o computador escolhe aleatoriamente. Os resultados são registrados entre sessões com contagens de vitórias, empates e derrotas exibidas no overlay.
- Estado Central do Jogo gameState.js#L11
Um gerenciador de estado construído manualmente que coordena toda a lógica de moedas sem um framework. Cada coleta segue um padrão consistente: atualizar o estado, persistir no localStorage, verificar a conclusão, sincronizar com o servidor e atualizar as métricas da comunidade. O estado das moedas é armazenado no localStorage para persistência entre visitas e sincronizado com o servidor a cada coleta. Isso mantém a experiência com estado sem exigir login, enquanto ainda registra eventos para as métricas da comunidade. Nenhuma biblioteca externa envolvida.
- Overlay de Resumo uiElements.js
O elemento de conexão que transforma três mini-games separados em uma experiência coerente. Acompanha o progresso da coleta, indica a localização de cada moeda e exibe a visão comunitária com estatísticas de participação ao vivo.
- Métricas Comunitárias ao Vivo gameState.js#L55
Cada coleta é registrada anonimamente no banco de dados, contribuindo para uma contagem comunitária ao vivo exibida a todos os visitantes. As métricas são armazenadas em cache a cada escrita e protegidas por rate limiting para manter os dados confiáveis.
// gameState.js
async collectCoverCoin() {
if (!this.flags.cover) {
this.flags.cover = true;
this.save(); // localStorage
this.checkCompletion();
await recordCoinCollected("boxCoin"); // POST /api/record-event
fetchAndDisplayMetrics();
trackEvent("boxCoin");
return true;
}
return false;
}
API
Cada coleta de moeda é registrada como um evento anônimo. Sem conta, sem dados pessoais, apenas um UUID armazenado em um cookie httpOnly que persiste entre visitas.
A cada coleta, o gameState.js escreve no localStorage para manter o estado do jogo no lado do cliente e então chama a API para registrar o evento no servidor. Os nomes das moedas são validados contra uma lista fixa antes da escrita, e o endpoint é protegido por rate limiting para evitar abusos.
O MetricsCalculator agrega a tabela coin_events em totais comunitários e armazena o resultado em cache por 12 horas, para que cada novo visitante receba os contadores da página inicial rapidamente, sem disparar uma nova consulta ao banco de dados a cada chegada.
# metrics_calculator.rb
def self.stats(cache: Rails.cache)
cache.fetch("coin_metrics", expires_in: 12.hours) { calculate_stats }
end
def self.calculate_stats
{
totalCoinsCollected: CoinEvent.count,
totalUsersWithCoins: CoinEvent.distinct.count(:client_id),
totalUsersWithAllThreeCoins: Client.joins(:coin_events)
.group("clients.id")
.having("COUNT(DISTINCT coin_events.coin_name) >= 3")
.count.length
}
end
0
moedas foram coletadas no total
por todos que participaram do jogo
Deploy
Um push para main inicia o fluxo completo de CI no GitHub Actions (.github/workflows/ci.yml). Varreduras de segurança (Brakeman, bundler-audit, importmap audit), uma verificação de linting com RuboCop e uma suíte de testes cobrindo respostas de controllers, validações de modelos, lógica de cache do MetricsCalculator e um smoke test com Selenium headless rodam em paralelo. Apenas uma build verde avança para o deploy.
O workflow de deploy (.github/workflows/deploy.yml) conecta via SSH, executa o build Docker multiestágio e sobe o stack com docker-compose. Uma lógica de retry foi adicionada após encontrar timeouts intermitentes de SSH nos runners do GitHub Actions.
Design
A decisão de construir o sistema de design manualmente, em vez de usar Tailwind ou Bootstrap, foi intencional. Este projeto foi uma oportunidade para desenvolver minhas habilidades de design e construir algo que fosse genuinamente meu do início ao fim. Um framework teria sido mais rápido, mas velocidade não era o objetivo aqui.
O resultado é um arquivo tokens.css que define toda a linguagem visual: tipografia, escala de cores, espaçamento e breakpoints, mantendo a consistência visual nos jogos, nas páginas de projeto e no layout bilíngue, sem dependência de framework.
Outras escolhas de design deliberadas foram: botões com estilo glass-morphism, uma barra de navegação inferior no mobile e um layout mobile-first totalmente desenvolvido no Figma antes de qualquer linha de CSS ser escrita.
Plano
Cada fase deste projeto foi tratada como um produto completo e pronto para lançamento antes de avançar. A fase de planejamento reflete uma decisão deliberada de projetar toda a experiência do usuário no Figma antes de escrever qualquer código. A fase de monitoramento no final não foi uma limpeza não planejada; representa o ponto em que a arquitetura estava estável o suficiente para adicionar polimento de produção, testes abrangentes e hardening de segurança.
A Fase 3 não fazia parte do plano original do projeto. Foi introduzida como uma etapa necessária após as restrições encontradas na Fase 2. A AWS parecia o ambiente de produção certo na época, mas a complexidade operacional e o custo de rodar EC2 e RDS para um projeto de portfólio se mostraram difíceis de justificar. Essa restrição levou à descoberta da abordagem de monolito Rails e à consolidação de tudo em um único VPS. O resultado foi uma solução mais adequada ao escopo deste projeto, que não teria sido encontrada sem passar pela Fase 2 primeiro.
Aprendizados
Este projeto nasceu com o objetivo de ser um portfólio vivo. Um lugar para demonstrar meu interesse em gamification e mostrar minhas habilidades como solutions engineer e desenvolvedor. Ao longo do caminho, surgiu uma pergunta mais específica: quanta disciplina de produção eu conseguiria aplicar a um site pessoal? A resposta abrangeu decisões de infraestrutura, arquitetura de back-end, design de gamification e deployment contínuo. Cada decisão foi tomada deliberadamente para refletir como eu trataria o sistema de qualquer cliente.
O resultado final é a página que você está visitando agora. A documentação que você esta lendo e o produto entregue são a mesma coisa.
Estes são os aprendizados mais significativos da construção deste projeto:
- Dimensionando a infraestrutura corretamente.
A necessidade de uma API externa revelou quantas opções de infraestrutura existem e como é fácil buscar mais do que um projeto realmente precisa. Implementar a AWS na Fase 2 me ensinou que uma abordagem cloud-native pode ser excessiva para o escopo errado.
Dois serviços gerenciados adicionaram custo e complexidade que o projeto não precisava. Essa restrição levou à descoberta da abordagem de monolito Rails. Combinado com um host Docker que eu controlo completamente, essa escolha se mostrou a melhor decisão de engenharia. A stack ficou mais simples de operar, mais fácil de reproduzir e mais adequada ao escopo real do projeto.
- Ruby on Rails permite que um dev solo trabalhe de forma enxuta.
A estrutura de monolito significou menos partes móveis para construir, monitorar e manter. Com o banco de dados diretamente conectado à camada de aplicação, o modelo de dados pôde evoluir sem negociar um contrato de API entre serviços.
Ter o front-end e o back-end em uma única codebase reduziu a troca de contexto e permitiu um fluxo de trabalho mais ágil ao longo do projeto. Rails não é a solução ideal para toda infraestrutura, mas para um desenvolvedor solo com controle total da stack, foi a decisão mais produtiva do projeto.
- Gamificação realmente brilha quando é mensurável.
Um dos principais objetivos era demonstrar como elementos simples de gamificação podem engajar visitantes de forma significativa. Os três mini-games criaram um fluxo coeso com um objetivo final claro, mas a gamificação só se tornou verdadeiramente atraente quando passou a ser mensurável.
Adicionar métricas comunitárias ao vivo baseadas na participação de visitantes anteriores foi a decisão que fechou o ciclo do usuário. Transformou o engajamento individual em um número compartilhado que todo visitante pode ver e para o qual pode contribuir. Na minha visão, essa camada comunitária é o que separa simples elementos de jogos de experiências de gamificação memoráveis.
- Um sistema de design de tokens compensa em todas as páginas.
Um único arquivo de tokens manteve cada página, jogo e layout bilíngue visualmente coerente, sem dependência de um framework. O escopo permaneceu pequeno, o bundle permaneceu leve, e qualquer alteração de estilo se propagou de forma consistente por todo o site.
A decisão de construir o sistema de design manualmente, em vez de usar um framework, foi deliberada. Este projeto foi uma oportunidade de desenvolver minhas próprias habilidades de design e construir algo inteiramente meu. Um framework teria sido mais rápido, mas a propriedade total da linguagem visual era o objetivo, e a estrutura de tokens provou ser a fundação certa para um projeto deste escopo.
- Tratar um projeto pequeno como produção é exatamente o ponto.
Este projeto foi desenvolvido com a mesma disciplina que eu aplicaria ao sistema de qualquer cliente. Hardening de segurança, rate limiting, uma suite de testes completa e um pipeline de CI/CD adequado são todos exercícios deliberados. Um portfólio pessoal não exige estritamente nenhum deles, mas os hábitos formados em projetos como esse são a base que qualquer sistema de produção sério exige.
Sistemas enterprise são infinitamente mais complexos, mas a atenção ao detalhe que os mantém sustentáveis começa precisamente com esse tipo de prática. A escala do projeto era pequena. O padrão aplicado não era.
Obrigado por conhecer mais sobre esse projeto!
Confira outros projetos:
JIKO App
Um app de produtividade com elementos de gamificação para monitorar sessões de foco e análise de crescimento pessoal.
N8N RSS Bot
Uma automação robusta que orquestra fluxos de registro de participantes e entrega atualizações de RSS via WhatsApp.
Trofy
Um app de tarefas que transforma seus objetivos pessoais em um jogo para reforçar o desenvolvimento de bons hábitos.
Born Survivor
O segundo jogo demo feito em Unity com uma mecânica no estilo “escolha 1 de 3”. Inspirado em Vampire Survivors.
Flappy Astronaut
Um jogo demo feito em Unity para aplicar conceitos de game-dev e C-sharp. Inspirado em Flappy Bird.