O Navistron persiste todos os seus dados — scores, sessões anônimas, patrocinadores e cliques — em um único banco MongoDB Atlas, usando o driver nativo do Node.js sem ORM. São 4 collections, 4 índices, 30+ aggregation pipelines e apenas 7 funções de consulta reutilizáveis. Neste artigo, vamos explorar cada camada dessa persistência.
4 Collections: O Que Cada Uma Armazena
O banco navistron contém exatamente 4 collections, cada uma com um propósito claro:
- scores — Partidas registradas com nome de piloto. 10 campos:
_id,name,score,tier,tierName,boosts,spread,time,difficulty,playedAt. Criada porPOST /api/scores - anonymous_sessions — Sessões anônimas (Unregistered/Unknown). 10 campos: substitui
nameportype. Criada porPOST /api/anonymous-sessions - sponsors — Patrocinadores. 7 campos:
_id,name,link,linkText,value(centavos),totalClicks,createdAt. Criada por admin ou seed - sponsor_clicks — Eventos de clique em links de patrocinadores. 5 campos:
_id,sponsorId(ObjectId),clickedAt,userAgent,referer. Criada porPOST /api/sponsors/click
A separação entre scores e anonymous_sessions é intencional: permite consultar registrados e anônimos independentemente, combinar ambos para totais, e manter o ranking limpo (apenas scores com nome).
Schemas: Campos, Tipos e Defaults
O MongoDB não exige schemas rígidos, mas o Navistron impõe consistência via sanitização no servidor. Cada POST constrói o documento com coerção explícita:
- name (scores) —
String(body.name || 'ANONYMOUS').toUpperCase().slice(0, 20). Forçado uppercase, máximo 20 caracteres, default "ANONYMOUS" - type (anonymous) — Validado contra
['Unregistered', 'Unknown']. Qualquer valor inválido vira'Unknown' - score, tier, boosts, time —
Number(body.field) || 0. Coerção numérica com default zero - spread, difficulty —
Number(body.field) || 1. Default 1 (valores mínimos do jogo) - tierName —
String(body.tierName || 'TIER I'). Default para o primeiro tier - playedAt —
new Date(). Sempre gerado no servidor, nunca confia no timestamp do client
Essa sanitização garante que mesmo requisições malformadas (campos faltantes, tipos errados, strings onde esperava números) resultem em documentos válidos e consistentes.
4 Índices para Queries Rápidas
O script seed.mjs cria 4 índices para otimizar as queries mais frequentes:
- scores:
{ score: -1 }— Otimiza a query mais comum: top scores ordenados do maior para o menor. Usado pelo ranking, home page, perfis de jogador - scores:
{ playedAt: -1 }— Otimiza queries por período (últimos 7 dias, 30 dias) e listagem de jogos recentes - scores:
{ name: 1 }— Otimiza buscas por jogador específico: perfil, histórico, cálculo de rank global - sponsors:
{ value: -1 }— Otimiza ordenação de patrocinadores por valor de contribuição
A collection anonymous_sessions não tem índices custom — as queries sobre ela usam scans ou aggregations que processam todos os documentos. Para o volume atual, isso é aceitável.
Connection Singleton: Um MongoClient por Container
A conexão com o MongoDB usa um padrão singleton em mongodb.js. A função getClientPromise() armazena a Promise de conexão no escopo do módulo:
- Primeira chamada — Cria
new MongoClient(uri), chama.connect(), armazena emclientPromise - Chamadas subsequentes — Retorna a Promise cacheada imediatamente (short-circuit)
- Em desenvolvimento — Armazena em
global._mongoClientPromisepara sobreviver ao HMR do Next.js - Em produção/serverless — Usa o escopo do módulo. Cada container Vercel mantém sua própria conexão enquanto estiver warm
Todos os consumidores seguem o mesmo padrão: const client = await getClientPromise(); const db = client.db('navistron'). O nome do banco é hardcoded — não configurável por variável de ambiente.
Client-Side: 3 Adapters para Fetch
O engine do jogo acessa o banco exclusivamente via API, usando 3 adapters internos:
- DB (scores) —
getAll()viaGET /api/scores,save(record)viaPOST /api/scores. Retorna{ id }do documento inserido, ou{ id: null }em caso de erro - AnonymousDB (sessões) —
save(session)viafetch(Skip/novo jogo),saveBeacon(session)vianavigator.sendBeacon(fechamento de aba) - SponsorsDB (patrocinadores) —
getAll()para listar,trackClick(sponsorId)para registrar cliques
Esse padrão adapter isola o jogo da implementação da API — o engine não sabe se os dados vão para MongoDB, PostgreSQL ou um arquivo. Apenas chama DB.save() e espera um id.
30+ Aggregation Pipelines: A Força do MongoDB
O verdadeiro poder do MongoDB no Navistron está nas aggregation pipelines. São 30+ pipelines distintos espalhados por 4 API routes e o data layer:
- /api/stats — 10 pipelines paralelos via
Promise.all: total de jogos, totalTime/avgScore/avgTime/highestScore ($group), pilotos únicos ($group+$count), ativos em 7 dias, jogos/dia ($dateToString), horários de pico ($hour), distribuição de tiers, top 10 jogadores, recentes, distribuição de scores ($bucket) - /api/anonymous-sessions GET — 7 pipelines: total, contagem por type, aggregates, sessões/dia por type (
$dateToString), distribuição de tiers, recentes 15, distribuição de scores ($bucketcom faixas 0–50, 50–100, ..., 50K+) - /api/admin/sponsors/stats — 7 pipelines incluindo 2 com
$lookuppara join entresponsor_clicksesponsors: cliques por patrocinador com nome, e cliques recentes com dados do patrocinador - data.js getAggregatedStats() — 9 pipelines combinando
scores+anonymous_sessionspara stats unificados - data.js getPlayerStats() — 3 queries: stats do jogador (
$match+$groupcom 11 acumuladores), rank global ($group+$match+$count), e top 50 scores
Os estágios mais usados são $group (agregação), $match (filtro por período), $sort + $limit (top-N), $dateToString (agrupamentos por dia), $bucket (distribuições) e $lookup (joins entre collections).
7 Funções de Consulta no Data Layer
O arquivo data.js centraliza todas as queries reutilizáveis do Server Components:
- getTopScores(limit=100) —
find({}).sort({ score: -1 }).limit(limit) - getScoresByPeriod(period, limit=100) — Filtra por
'weekly'(7 dias),'monthly'(30 dias) ou'all' - getAllPlayerNames() —
aggregate([{ $group: { _id: '$name' } }, { $sort: { _id: 1 } }]) - getPlayerStats(nickname) — 3 queries paralelas: stats, rank global, top 50 scores
- getSponsors() —
find({}).sort({ value: -1 }) - getAggregatedStats(period) — 9 aggregations combinando ambas as collections
- formatTime/formatValue/siteUrl — Utilitários de formatação
Cada função é async, chama getClientPromise() lazily, e serializa _id para strings antes de retornar (necessário para passar dados entre Server Components e Client Components no Next.js).
Seed Script: Índices e Dados Iniciais
O scripts/seed.mjs é executado uma única vez via npm run seed para configurar o banco:
- Parse manual do .env — Lê o arquivo com
readFileSync, parseia linha a linha (sem dependênciadotenv) - Cria índices — 3 em
scores+ 1 emsponsorsviacreateIndex() - Seed de patrocinadores — Insere 2 documentos iniciais via
insertMany(), apenas secountDocuments()retornar 0 (idempotente)
O script não é executado durante o deploy — apenas manualmente. É seguro rodar múltiplas vezes: os índices são criados com createIndex (que é no-op se já existem) e os sponsors só são inseridos se a collection estiver vazia.
Operações MongoDB Usadas
O Navistron utiliza 8 operações diferentes do driver nativo, cobrindo todo o espectro CRUD + analytics:
- find() — Leitura com sort, limit e filtros de data. Usado em 10+ pontos
- insertOne() — Criação de scores, sessões anônimas, sponsors e cliques
- insertMany() — Seed de patrocinadores iniciais
- updateOne() + $set — Edição de scores e sponsors pelo admin
- updateOne() + $inc — Incremento atômico de
totalClicksem sponsors - deleteOne() — Remoção de scores e sponsors pelo admin
- aggregate() — 30+ pipelines com
$group,$match,$bucket,$lookup,$unwind,$dateToString,$hour,$count - countDocuments() — Contagens simples com filtros opcionais
FAQ — Perguntas Frequentes sobre o Banco de Dados
Por que MongoDB e não PostgreSQL ou outro relacional?
O MongoDB é ideal para o Navistron por três razões: (1) os dados de gameplay têm schema flexível e podem evoluir sem migrações; (2) as aggregation pipelines permitem analytics complexos (distribuições, agrupamentos por dia/hora, joins) sem SQL; (3) o driver nativo permite queries diretas sem ORM, mantendo o projeto com zero dependências extras para acesso a dados.
Os dados são sanitizados antes de salvar?
Sim, duplamente. O client valida no game engine (nome uppercase, max 20 chars, tipo de sessão) e o servidor revalida tudo: Number() com defaults seguros para campos numéricos, String().toUpperCase().slice(0, 20) para nomes, validação de type contra whitelist para sessões anônimas, e playedAt gerado no servidor via new Date() (nunca confia no clock do client).
Como o timestamp é gerado?
O campo playedAt é sempre new Date() no servidor — tanto para scores quanto para sessões anônimas. O client não envia timestamp. Isso garante consistência temporal e impede manipulação de datas por jogadores.
Quantas aggregation pipelines existem no total?
Mais de 30 pipelines distintos distribuídos em 4 API routes e o data layer. O maior concentrador é /api/stats com 10 pipelines paralelos via Promise.all(). Os estágios mais complexos incluem $bucket (distribuição de scores em 10 faixas) e $lookup (join entre sponsor_clicks e sponsors para analytics de cliques).
O que acontece se o MongoDB estiver offline?
As API routes retornam erro 500 com mensagem genérica. O jogo funciona normalmente no client (é Canvas 2D puro), mas não consegue salvar scores nem carregar o leaderboard. Scores não salvos durante uma falha são perdidos — não há fila local ou retry. O ISR da Vercel continua servindo a última versão cacheada das páginas estáticas.
