Este documento descreve a arquitetura técnica do projeto Magic3T, um jogo multiplayer em tempo real com sistema de rating/ranking.
| Camada | Tecnologia |
|---|---|
| Frontend | React 19, TypeScript, Vite, TanStack Router, TanStack Query, Tailwind CSS, Radix UI |
| Backend | NestJS, TypeScript, WebSockets (Socket.IO) |
| Database | Firebase Firestore, PostgreSQL (via pg) |
| Autenticação | Firebase Authentication (Google Provider) |
| Observabilidade | Sentry (Error tracking, Performance monitoring) - Backend e Frontend |
| Monorepo | npm Workspaces |
| Linting | Biome |
| Deploy | Render (backend), Vercel (frontend) |
Magic3T/
├── backend/ # API NestJS
├── frontend/ # App React/Vite
├── packages/ # Bibliotecas compartilhadas
│ ├── api-types/ # Tipos de API (DTOs, eventos WebSocket)
│ ├── common-types/ # Tipos comuns (Team, Choice, Rating)
│ ├── database-types/ # Tipos de entidades do banco (UserRow, MatchRow)
│ └── migrations/ # Migrações SQL do PostgreSQL
├── biome.json # Configuração do linter
├── package.json # Workspaces do monorepo
└── render.yaml # Configuração de deploy
backend/src/
├── instrument.ts # ⚡ Instrumentação (Sentry) - DEVE ser importado primeiro
├── main.ts # Bootstrap da aplicação
├── app.module.ts # Módulo raiz - importa todos os outros
├── app.gateway.ts # WebSocket gateway principal
├── app.controller.ts # Controller de health check
│
├── infra/ # 🏗️ Infraestrutura (external services)
│ ├── infrastructure.module.ts # Módulo agregador (@Global) - exporta todos abaixo
│ │
│ ├── database/ # 🐘 PostgreSQL (raw queries via `pg`)
│ │ ├── database.module.ts
│ │ └── database.service.ts # Pool de conexões, query(), transaction()
│ │
│ ├── firestore/ # 💾 Firebase Firestore
│ │ ├── firestore.module.ts
│ │ ├── firestore.service.ts
│ │ └── repositories/ # Repositories por entidade
│ │ ├── base-repository.ts
│ │ ├── user/
│ │ ├── match/
│ │ ├── config/
│ │ └── crash-report/
│ │
│ ├── firebase/ # 🔥 Integração Firebase
│ │ ├── firebase.module.ts
│ │ └── firebase.service.ts
│ │
│ └── websocket/ # 🔌 Infraestrutura WebSocket
│ ├── websocket.module.ts
│ ├── websocket-emitter.service.ts
│ └── types.ts
│
├── modules/ # 📦 Módulos de Negócio
│ ├── auth/ # 🔐 Autenticação
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts
│ │ ├── auth.guard.ts
│ │ ├── auth-request.ts
│ │ ├── auth-socket.ts
│ │ ├── skip-auth.decorator.ts
│ │ └── user-id.decorator.ts
│ │
│ ├── match/ # 🎮 Lógica de Partidas
│ │ ├── match.module.ts
│ │ ├── match.service.ts
│ │ ├── match.controller.ts
│ │ ├── match.gateway.ts
│ │ ├── match.guard.ts
│ │ ├── client-sync.service.ts
│ │ ├── persistance.service.ts
│ │ ├── lib/ # Lógica do jogo (Match, MatchBank)
│ │ ├── bots/ # Implementações de bots
│ │ └── events/ # Eventos internos
│ │
│ ├── queue/ # ⏳ Fila de Matchmaking
│ │ ├── queue.module.ts
│ │ ├── queue.service.ts
│ │ ├── queue.controller.ts
│ │ └── queue.gateway.ts
│ │
│ ├── rating/ # ⭐ Sistema de Rating/ELO
│ │ ├── rating.module.ts
│ │ ├── rating.service.ts
│ │ └── rating-converter.ts
│ │
│ ├── user/ # 👤 Usuários
│ │ ├── user.module.ts
│ │ ├── user.service.ts
│ │ └── user.controller.ts
│ │
│ └── admin/ # 🛡️ Administração
│ ├── admin.module.ts
│ ├── admin.controller.ts
│ ├── admin.guard.ts
│ └── admin.service.ts
│
├── common/ # 🔧 Utilitários Compartilhados
│ ├── decorators/ # Decorators customizados
│ │ └── gateway-event.decorator.ts
│ ├── errors/ # Classes de erro
│ ├── filters/ # Exception filters
│ │ ├── response-error.filter.ts
│ │ ├── unexpected-error.filter.ts
│ │ └── throttling.filter.ts
│ ├── guards/ # Guards compartilhados
│ │ └── ws-throttler.guard.ts
│ ├── pipes/ # Validation pipes
│ ├── websocket/ # Classes base WebSocket
│ │ └── base.gateway.ts
│ └── utils/ # Funções utilitárias
│
└── shared/ # 📦 Tipos Internos Compartilhados
├── types/ # Tipos utilitários
└── websocket/ # Tipos WebSocket
└── namespaces-map.ts
┌─────────────────┐
│ AppModule │
└────────┬────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ infra/ │ │ modules/ │ │ common/ │
│ (Firebase, │◄──────│ (Auth, Match, │────────►│ (BaseGateway, │
│ Database, │ │ Queue, User, │ │ Guards, │
│ WebSocket) │ │ Rating, Admin)│ │ Filters) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Fluxo detalhado dos módulos:
┌──────────────────────────────────┐
│ InfrastructureModule │
│ (Firebase, Firestore, Database, │
│ WebSocket) │
└────────────────┬─────────────────┘
│ exporta todos
┌─────────┐ │
│ Auth │◄────────────────────┤
└─────────┘ │
│ │
└──────────────────────────┤
│
┌──────────┬──────────┴───────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Match │◄─│ Queue │ │ User │
└─────────┘ └──────────┘ └─────────┘
│ │
▼ │
┌─────────┐ │
│ Rating │◄─────┘
└─────────┘
frontend/src/
├── main.tsx # Entry point
├── main.css # Estilos globais (Tailwind)
├── route-tree.gen.ts # Rotas geradas automaticamente
│
├── routes/ # 📍 Páginas (TanStack Router)
│ ├── __root.tsx # Layout raiz
│ ├── index.tsx # Home
│ ├── game.$matchId.tsx # Tela de partida
│ ├── profile.$slug.tsx # Perfil de usuário
│ └── ...
│
├── components/ # 🧱 Componentes React
│ ├── atoms/ # Componentes básicos (Button, Input)
│ └── ... # Componentes compostos
│
├── contexts/ # 🌐 Contextos React
│ ├── auth-context.tsx # Estado de autenticação
│ ├── game-context.tsx # Estado da partida atual
│ ├── queue.context.tsx # Estado da fila
│ └── ...
│
├── services/ # 🔌 Comunicação com Backend
│ ├── firebase.ts # Inicialização Firebase Client
│ └── clients/
│ ├── api-client.ts # Clientes REST (UserApiClient, MatchApiClient)
│ └── base-api-client.ts # Cliente base com auth headers
│
├── hooks/ # 🪝 Custom Hooks
├── lib/ # 📚 Utilitários
├── types/ # 📦 Tipos do frontend
└── assets/ # 🖼️ Imagens, fontes
Tipos compartilhados entre frontend e backend:
- DTOs de controllers (requests/responses)
- Namespace
Admin— tipos do painel administrativo (ListAccountsResult,ListAccountsResultItem) - Eventos WebSocket (QueueServerEvents, MatchServerEvents)
- Tipos de erro
Tipos de domínio do jogo:
Team- Order/ChaosChoice- Escolhas do jogo (1-9)Rating- Estrutura de rating
Tipos de entidades do Firestore:
UserRow- Documento de usuárioMatchRow- Documento de partidaBotConfig- Configuração de bots
Sistema de migrações SQL do PostgreSQL:
- Migrações versionadas por timestamp em arquivos
.sqlpuros - Runner transacional (aplica todas as pendentes ou nenhuma)
- Tabela de controle
_migrationsno banco - GitHub Action para deploy automático em produção
- Documentação completa em
packages/migrations/MIGRATIONS.md
┌──────────────────────────────────────────────────────────────────────────┐
│ FLUXO DE AUTENTICAÇÃO │
└──────────────────────────────────────────────────────────────────────────┘
1. LOGIN (Frontend → Firebase)
┌──────────┐ signInWithPopup() ┌──────────────┐
│ Frontend │ ────────────────────────────►│ Firebase Auth│
│ (React) │◄──────────────────────────── │ (Google) │
└──────────┘ ID Token + User Info └──────────────┘
2. CHAMADA AUTENTICADA (Frontend → Backend)
┌──────────┐ Authorization: Bearer <token> ┌──────────┐
│ Frontend │ ─────────────────────────────────►│ Backend │
│ │ │ (NestJS) │
└──────────┘ └────┬─────┘
│
3. VALIDAÇÃO DO TOKEN (Backend → Firebase Admin) │
▼
┌──────────┐ verifyIdToken(token) ┌──────────────┐
│ Backend │ ───────────────────────────────►│ Firebase Auth│
│ AuthGuard│◄─────────────────────────────── │ Admin │
└──────────┘ { uid: "user123" } └──────────────┘
4. ACESSO AUTORIZADO
┌──────────┐ Resposta ┌──────────┐
│ Frontend │◄───────────────────────────────│ Backend │
└──────────┘ └──────────┘
| Componente | Responsabilidade |
|---|---|
frontend/services/firebase.ts |
Inicializa Firebase Client SDK |
frontend/lib/auth-client.ts |
Gerencia sessão, obtém tokens |
frontend/contexts/auth-context.tsx |
Estado de auth no React |
frontend/services/clients/base-api-client.ts |
Injeta token em requests |
backend/src/modules/auth/auth.guard.ts |
Intercepta requests, valida token |
backend/src/modules/auth/auth.service.ts |
Chama Firebase Admin para validar |
backend/src/infra/firebase/firebase.service.ts |
Conexão com Firebase Admin |
// HTTP Requests
headers: { Authorization: `Bearer ${idToken}` }
// WebSocket Connections
socket.handshake.auth = { token: idToken }┌──────────────────────────────────────────────────────────────────────────┐
│ FLUXO DE MATCHMAKING │
└──────────────────────────────────────────────────────────────────────────┘
Player A Backend Player B
│ │ │
│──── WS: join queue ─────►│ │
│ │◄──── WS: join queue ─────│
│ │ │
│ ┌─────┴─────┐ │
│ │QueueService│ │
│ │ encontra │ │
│ │ match │ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │MatchService│ │
│ │ cria match │ │
│ └─────┬─────┘ │
│ │ │
│◄── WS: match.found ──────┼────── WS: match.found ──►│
│ │ │
Player A Backend Player B
│ │ │
│──── WS: match.move ─────►│ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Match │ │
│ │ (lib) │ │
│ │ processa │ │
│ └─────┬─────┘ │
│ │ │
│◄── WS: match.sync ───────┼────── WS: match.sync ───►│
│ │ │
│ [Partida termina] │
│ │ │
│ ┌─────┴─────┐ │
│ │Persistance │ │
│ │ Service │ │
│ │ salva match│ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Rating │ │
│ │ Service │ │
│ │atualiza ELO│ │
│ └─────┬─────┘ │
│ │ │
│◄── WS: match.end ────────┼────── WS: match.end ────►│
| Gateway | Namespace | Responsabilidade |
|---|---|---|
AppGateway |
/ |
Conexão geral, heartbeat |
QueueGateway |
/queue |
Fila de matchmaking |
MatchGateway |
/match |
Partidas em tempo real |
// Cliente → Servidor
'queue.join' // Entrar na fila
'queue.leave' // Sair da fila
// Servidor → Cliente
'queue.accepted' // Fila aceita
'queue.matchFound' // Match encontrado// Cliente → Servidor
'match.move' // Fazer uma jogada
'match.forfeit' // Desistir
// Servidor → Cliente
'match.sync' // Sincronizar estado
'match.end' // Partida terminouO DatabaseService gerencia um pool de conexões com o PostgreSQL via pg.
| Método | Descrição |
|---|---|
query<T>(text, values?) |
Executa uma query parametrizada e retorna as linhas |
transaction<T>(callback) |
Executa múltiplas queries em uma transação (com BEGIN/COMMIT/ROLLBACK automático) |
SSL é habilitado automaticamente em produção (PG_HOST !== 'localhost').
firestore/
├── users/ # Usuários
│ └── {userId}/
│ ├── identification # Nickname, slug
│ ├── elo # Rating atual
│ ├── stats # Estatísticas
│ └── role # user | admin | bot
│
├── matches/ # Histórico de partidas
│ └── {matchId}/
│ ├── players # IDs dos jogadores
│ ├── result # Resultado
│ ├── moves # Histórico de jogadas
│ └── timestamp # Data/hora
│
├── config/ # Configurações globais
│ ├── rating # Configuração do sistema de rating
│ └── bots/ # Configurações de bots
│
└── crash-reports/ # Relatórios de erro
O sistema usa ELO modificado com Leagues:
ELO → RatingConverter → { league, division, lp, tier }
- Bronze
- Silver
- Gold
- Platinum
- Diamond
- Master
- Challenger (top players)
Partida termina
│
▼
MatchFinishedEvent
│
▼
RatingService.updateRating()
│
▼
UserRepository.updateElo()
# Raiz do monorepo
npm install # Instala deps de todos os workspaces
npm run lint # Roda Biome em todo o projeto
npm run migrate # Aplica migrações SQL pendentes (local)
# Backend
cd backend
npm run start:dev # Dev server com hot reload
npm run build # Build de produção
npm run test # Testes com Vitest
# Frontend
cd frontend
npm run dev # Dev server Vite
npm run build # Build de produção
# Migrations
cd packages/migrations
npm run new <nome> # Cria nova migração (up.sql + down.sql)
npm run migrate # Aplica migrações pendentes (usa .env local)PORT=3000
FIREBASE_ADMIN_CREDENTIALS=<base64 do JSON de credenciais>
FIRESTORE_DB=<nome do database>
# PostgreSQL
PG_HOST=localhost
PG_PORT=5432
PG_USER=dev
PG_PASSWORD=let-me-in
PG_DATABASE=magic3tPara desenvolvimento local, suba o PostgreSQL com
docker compose up -d.
# URL do backend
VITE_API_URL=http://localhost:4000
# URL do CDN para assets
VITE_CDN_URL=https://storage.googleapis.com/your-bucket.appspot.com
# Firebase Credentials
VITE_FIREBASE_API_KEY=your-api-key-here
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
VITE_FIREBASE_APP_ID=your-app-id
VITE_FIREBASE_MEASUREMENT_ID=your-measurement-id
⚠️ Importante: Nunca commite credenciais reais. Use o arquivo.env.examplecomo template.
O projeto implementa múltiplas camadas de segurança tanto para requisições HTTP quanto WebSocket.
O backend utiliza o middleware Helmet para configurar headers HTTP de segurança:
// main.ts
import helmet from 'helmet'
app.use(helmet())Headers configurados automaticamente:
X-Content-Type-Options: nosniffX-Frame-Options: DENYStrict-Transport-Security(HSTS)X-XSS-Protection
O CORS está configurado para aceitar apenas origens autorizadas, centralizadas em um arquivo de configuração:
Arquivo de configuração:
// backend/src/shared/constants/cors.ts
export const CORS_ALLOWED_ORIGINS = [
'https://magic3t.com.br',
'https://www.magic3t.com.br',
'http://localhost:3000', // Desenvolvimento
]Uso em HTTP requests:
// backend/src/main.ts
import { CORS_ALLOWED_ORIGINS } from './shared/constants/cors'
app.enableCors({
origin: CORS_ALLOWED_ORIGINS,
credentials: true,
})Uso em WebSocket Gateways:
// backend/src/modules/match/match.gateway.ts
import { CORS_ALLOWED_ORIGINS } from '@/shared/constants/cors'
@WebSocketGateway({
cors: { origin: CORS_ALLOWED_ORIGINS, credentials: true },
namespace: 'match'
})
export class MatchGateway extends BaseGateway { ... }Todos os gateways (AppGateway, QueueGateway, MatchGateway) importam esta mesma constante, garantindo consistência e facilitando manutenção.
O NestJS ThrottlerModule limita requisições HTTP:
ThrottlerModule.forRoot({
throttlers: [
{ name: 'short', limit: 3, ttl: 1000 }, // 3 req/segundo
{ name: 'medium', limit: 20, ttl: 10000 }, // 20 req/10 segundos
{ name: 'long', limit: 100, ttl: 60000 }, // 100 req/minuto
]
})Um guard customizado protege os WebSocket gateways contra abuso:
// common/guards/ws-throttler.guard.ts
@Injectable()
export class WsThrottlerGuard extends ThrottlerGuard {
async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
// Identifica cliente pelo IP
const tracker = client.handshake.address
// Incrementa contador e verifica limite
// Bloqueia se exceder limite
}
}Todas as requisições passam por validação automática:
app.useGlobalPipes(new ValidationPipe())export class ChangeNickCommandClass {
@IsDefined()
@IsString()
@MinLength(3)
@MaxLength(16)
@Matches(/^[a-zA-Z0-9áÁâÂ...]*$/)
nickname: string
}Mensagens de chat são validadas e sanitizadas:
const MAX_MESSAGE_LENGTH = 500
// Valida tipo e tamanho
if (!body || typeof body !== 'string' || body.length > MAX_MESSAGE_LENGTH) {
return
}
// Sanitiza conteúdo
const sanitizedMessage = body.trim().slice(0, MAX_MESSAGE_LENGTH)Filters globais garantem tratamento consistente de erros:
| Filter | Propósito |
|---|---|
UnexpectedErrorFilter |
Captura erros não tratados, retorna 500 genérico |
ResponseErrorFilter |
Formata erros esperados com errorCode |
ThrottlingFilter |
Trata exceção de rate limit |
A autenticação WebSocket é feita durante a conexão:
// BaseGateway.handleConnection()
async handleConnection(client: Socket) {
const token = client.handshake.auth.token
const userId = await this.authService.validateToken(token)
if (!userId) {
client.send('error', { errorCode: 'unauthorized' })
client.disconnect()
return
}
client.data.userId = userId
client.join(`user:${userId}@${this.namespace}`)
}| Prática | Status | Detalhes |
|---|---|---|
| Credenciais em variáveis de ambiente | ✅ | Firebase config via import.meta.env |
| CORS restrito | ✅ | Apenas domínios autorizados |
| Rate limiting HTTP | ✅ | ThrottlerModule configurado |
| Rate limiting WebSocket | ✅ | WsThrottlerGuard customizado |
| Headers de segurança | ✅ | Helmet middleware |
| Validação de entrada | ✅ | ValidationPipe + class-validator |
| Container não-root | ✅ | USER node no frontend/Dockerfile |
| Sanitização de mensagens | ✅ | Limite de tamanho e trim |
- Arquivos:
kebab-case.ts - Classes/Types:
PascalCase - Funções/variáveis:
camelCase - Eventos WebSocket:
namespace.action(ex:match.move)
Cada módulo segue a estrutura:
module-name/
├── module-name.module.ts # Definição do módulo
├── module-name.service.ts # Lógica de negócio
├── module-name.controller.ts # Endpoints REST
├── module-name.gateway.ts # WebSocket (se aplicável)
├── dtos/ # Data Transfer Objects
├── types/ # Tipos internos
└── index.ts # Exports públicos
Cada pasta com index.ts exporta sua API pública:
// firestore/index.ts
export * from './database.module'
export * from './database.service'
export * from './user'
export * from './match'Constantes que são usadas em múltiplos lugares devem ser centralizadas em backend/src/shared/constants/:
| Arquivo | Uso |
|---|---|
cors.ts |
Origens CORS permitidas - importada em main.ts e todos os gateways |
Exemplo:
// backend/src/shared/constants/cors.ts
export const CORS_ALLOWED_ORIGINS = [
'https://magic3t.com.br',
'https://www.magic3t.com.br',
'http://localhost:3000',
]
// Uso em qualquer lugar
import { CORS_ALLOWED_ORIGINS } from '@/shared/constants/cors'