Visão Geral da Arquitetura
Plataforma multi-tenant de apostas esportivas (Cactus Gaming). ~30 sites clientes, cada um com seu fork do template base.
Repositórios
| Repo | Descrição |
|---|---|
front-cactus-core | SDK TypeScript — pacotes @cactus-agents/* (monorepo) |
front-web-base | Template React Router v7 (SSR / Cloudflare Workers) |
front-cactus-docs | Esta documentação (Docusaurus) |
front-web-{marca} | Forks de marca (ex: front-web-vera-bet-br) |
front-web-panel | Painel admin (vault + deploys) |
front-ops | Reusable workflow de deploy + configuração por brand/repo/ambiente |
Stack
| Camada | Tecnologia |
|---|---|
| Framework | React Router v7 (SSR nativo no Cloudflare Workers) |
| Build/bundler | Vite (via React Router v7) |
| Styling | Tailwind CSS v3 + CSS custom properties para tokens de tema por cliente |
| State (client) | Zustand (13 stores: auth, layout, brand, gamification, kyc, validationSteps, validationRuntime, passwordValidation, games, payments, wallet, user, accountMenu) |
| State (server) | React Router loaders |
| i18n | i18next + react-i18next (traduções) + @cactus-agents/i18n |
| Ícones | lucide-react |
| Fonte | @fontsource/montserrat |
| Flags | country-flag-icons |
| Linter/formatter | Biome (todos os repos) |
| Testes | Vitest + React Testing Library |
| Pre-commit | Husky + lint-staged → biome check --write |
| Commits | Conventional Commits (commitlint) |
| Package manager | pnpm (enforced) |
| Node | >=24.0.0 (recomendado: 24.14.1) |
SDK — Pacotes @cactus-agents/*
| Pacote | Descrição |
|---|---|
api-client | ApiClient base (fetch, headers, auth) |
auth | AuthService (login, register, logout, profile) |
brand | BrandConfig (appearance, features, settings) |
country-config | Configuração por país (moeda, locale, validações) |
i18n | Internacionalização (setup i18next, namespaces) |
games | GamesService (home, categorias, providers, busca) |
gamification | Smartico SDK integration |
kyc | KYC (operadores, iframe, polling) |
payments | PaymentsService (deposit, withdraw, providers) |
sports | SportsService (Altenar, Betby) |
types | Tipos compartilhados |
user | UserService (perfil, segurança, limites) |
utils | Utilitários compartilhados |
validations | Validações (modules, contexts, runtime) |
wallet | WalletService (saldo, transações, rollover) |
Deploy
- SSR via Cloudflare Workers (React Router v7)
- Assets estáticos → Cloudflare R2
- 1 Worker por ambiente (SSR + static, sem api-proxy separado)
- Deploy centralizado via reusable workflow no
front-ops - Forks chamam
cactus-agents/front-ops/.github/workflows/deploy.yml@main - Configuração em
config/brands/<brand>/{brand.yml,repos.yml}mapeia repo → environments → worker_name (com defaults por brand) - Secrets de deploy (Cloudflare) são org secrets com acesso restrito
Pacotes privados
- GitHub Packages como registry privado
- Token de leitura por cliente (acesso apenas a
@cactus-agents/*, sem código-fonte)
Modelo de fork
O fork é uma cópia completa do front-web-base. O cliente tem liberdade total para personalizar: criar componentes, páginas, regras, redesenhar o tema inteiro.
Pontos rápidos de customização (já preparados no template):
app/config/theme/colors.ts— paleta de coresapp/config/layout/composition.ts— composição do layout (slots e variantes por brand)app/config/routes/paths.ts— customização de URL paths por brand
O registro de rotas fica em app/router/routes.ts (não overrideable). A árvore de variantes de layout vive em app/layouts/variants/ (não overrideable).
Mas o cliente não está limitado a esses arquivos. Pode modificar qualquer coisa no fork.
Única restrição: não alterar pacotes @cactus-agents/* diretamente — eles são dependências npm atualizáveis via pnpm update.
Regra arquitetural: CORE vs BASE
O
front-web-baseé burro por design. Ele chama métodos do SDK, recebe dados normalizados e renderiza. Toda inteligência de domínio vive nos pacotes@cactus-agents/*.
O que vai onde
| Se é... | Onde vive |
|---|---|
| Shape real da API (snake_case) | @cactus-agents/<pkg>/src/ como RawXxx |
| Tipo público normalizado (camelCase) | @cactus-agents/<pkg>/src/types.ts |
| Transform raw → público | @cactus-agents/<pkg>/src/transform.ts |
| Constante de domínio (opções de seleção, etc.) | @cactus-agents/<pkg>/src/ exportada |
Helper de cálculo (parseLimitPeriod, etc.) | @cactus-agents/<pkg>/src/ exportado |
| Validador de formato (CPF, CLABE, RUT...) | @cactus-agents/country-config/src/validators/ |
| Lógica condicional por país | @cactus-agents/country-config/src/countries/ |
| Feature flag da API | @cactus-agents/brand/src/transform/features.ts |
| Proxy HTTP server-side | front-web-base/app/routes/api.*.ts (simples, sem lógica) |
| Renderização de UI | front-web-base/app/components/**/*.tsx |
Padrão Raw → Transform → Público
API (snake_case) → RawXxx { campo_api } → transformXxx() → Xxx { campoNormalizado }
Exemplo: Login History
- A API retorna
date: "2026-03-17 18:14:42"— o SDK converte paracreatedAt: "2026-03-17T18:14:42" - A API retorna
city+stateseparados — o SDK compõelocation: "São Paulo, SP" - O template só usa
item.createdAteitem.location— zero normalização local
Documentação detalhada: CORE vs BASE — Arquitetura
Regra arquitetural: app/config
A pasta app/config no template deve conter somente configuração estática de brand (dados que variam por fork/marca):
- ✅ Objetos de config exportados (ex:
sportsConfig,gamificationConfig,depositConfig) - ❌ Tipos e interfaces de config — ficam em
app/types/(importados pelos configs) - ❌ Hooks (devem ficar em
app/hooks/) - ❌ Parsers, helpers, lógica de negócio
- ❌ Registros estruturais (registro de rotas →
app/router/, registry de variantes →app/layouts/) - ❌ Dados globais (catálogo de países, DDI, currency-country) — vêm do SDK
@cactus-agents/*
Overrides de brand são resolvidos pelo brandOverridesPlugin do Vite: qualquer import ~/xxx é checado primeiro em overrides/<brand-key>/app/xxx e, se o arquivo existir lá, substitui o base. Não há whitelist — qualquer arquivo sob ~/ é potencialmente overridável. Os alvos mais comuns estão em forking/override-files.
Tipos de config ficam em app/types/ (ex: navigation.ts, layout.ts, licenses.ts, deposit.ts, topbar.ts). Configs e overrides importam tipos diretamente de ~/types/<file>.
Decisões técnicas
| Decisão | Motivo |
|---|---|
| Cookie HttpOnly para JWT | Token não fica acessível no browser JS; leitura e validação acontecem no server. |
| AuthService singleton client-side | Um único ApiClient pro browser, separado do SSR. |
| Zustand sem persist para auth | Store mantém apenas user/userInfo/authModal/hydrated; estado inicial vem do loader SSR. |
| 13 Zustand stores separadas | Cada domínio tem sua store isolada. Evita store monolítica e facilita code-splitting. |
| Modais controlados por Zustand | authModal: 'login' | 'register' | null. Qualquer componente abre/fecha. |
| EnvProvider separado do BrandProvider | clientEnv (API_BASE_URL etc.) precisa existir antes de qualquer chamada API client-side. |
| Fork = cópia completa + liberdade total | Config files são atalhos rápidos, não limites. Cliente pode personalizar o que quiser. |
| Biome em vez de ESLint | Mais rápido, config única pra lint + format, sem plugins. |
| i18next para i18n | Biblioteca consolidada com suporte a namespaces, interpolação e pluralização. |