Visão Geral da Arquitetura
Plataforma multi-tenant de apostas esportivas (Cactus Gaming). Um único template
(front-web-base) serve 13 marcas via diretórios de override, distribuídas
em 28 ambientes de deploy. Não há nenhum fork ativo.
:::warning Antes de continuar: leia Trunks de marca
O front-web-base opera com várias branches de longa duração em paralelo
(main, main-7k, main-b, …), e features são portadas entre elas, não
mergeadas. Estas docs descrevem a main-7k — parte do que está aqui pode não
existir na branch em que você está.
:::
Repositórios
Lista viva em feca.yaml na raiz do workspace. Hoje:
| Repo | Descrição |
|---|---|
front-cactus-core | SDK TypeScript — pacotes @cactus-agents/* (monorepo) |
front-web-base | Template React Router v7 (SSR / Cloudflare Workers) — 13 marcas em overrides/ |
front-cactus-docs | Esta documentação (Docusaurus) |
front-ops | Reusable workflows de deploy + config por brand/repo/ambiente |
front-service-api | Worker proxy da API (Smart Placement + cache por path) |
front-service-api-dev-proxy | Dev proxy (domain remap + auth por dev) |
front-service-api-warmup | Worker de warmup de cache (cron) |
fecadm | Admin CLI + web dashboard (CF workers, Access, Zero Trust) |
front-tests-e2e | Testes E2E Playwright |
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 — ~28 stores em app/store/ (lista viva) + useAccountsStore do core (auth, usuário, wallet, transações, coins) |
| State (server) | React Router loaders |
| i18n | i18next + react-i18next (traduções) + @cactus-agents/i18n |
| Ícones | unplugin-icons + datasets Iconify (lucide, mdi, simple-icons) + SVGs custom em app/icons/custom/. Ver Icon System |
| 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/*
Lista viva em front-cactus-core/packages/. Hoje são 16 pacotes publicados:
| Pacote | Descrição |
|---|---|
accounts | Auth, usuário, wallet, recovery, responsible gaming + a store useAccountsStore (/react) |
api-client | ApiClient base (fetch, headers, auth, extractApiError) |
brand | BrandConfig (appearance, features, settings) + selectByCountry |
country-config | Configuração por país (moeda, locale, validadores) |
games | GamesService (home, categorias, providers, busca) |
gamification | Smartico SDK integration |
i18n | Internacionalização (setup i18next, namespaces) |
kyc | KYC (operadores, iframe, polling) |
mocks | DEPRECATED — stub vazio; substituído por integrações reais de API |
payments | PaymentsService (deposit, withdraw, providers) |
platform-cache | Engine de cache multi-tier do brand worker |
sports | SportsService (providers: first, altenar, betby, rogue) |
sweepstakes | Fluxo sweepstakes / dual-currency |
types | Tipos compartilhados |
utils | Utilitários compartilhados (imagens, forms, money) |
validations | Validações (modules, contexts, runtime) |
:::note Pacotes removidos e dependências externas
@cactus-agents/auth, /user e /wallet foram unificados em accounts em
2026-04-17; os diretórios seguem no repo como cascas vazias. E não presuma que
todo @cactus-agents/* mora no core: @cactus-agents/sports-rogue é dependência
real publicada de outro lugar.
:::
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 - O
ci-deploy.ymldo base dispara em globs de branch (main,main-*,stage,stage-*,sports,sports-*) — ver Trunks de marca - Configuração em
config/brands/<repo>/brand.yml(identidade +repo:) eenvironments/<env>/deploy.ymlmapeia ambiente →worker_name+default_branch+origin_domain. Ver Deploy - 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 marca: um repo + overrides/
Não existe um repo por marca. As 13 marcas vivem como diretórios de override
dentro do próprio front-web-base:
overrides/
├── 7k-bet-br/ ├── pb-bet/
├── betpontobet-bet-br/ ├── ph-state77-com/
├── casateste-com/ ├── pt-state77-com/
├── cl-bet7k-com/ ├── rj-bet/
├── donald-bet-br/ ├── state77-com/
├── fi-7k-bet/ └── x2b-bet/
├── ng-7k-bet/
A marca ativa é resolvida em build time pelo brandOverridesPlugin
(vite-plugins/brand-overrides.ts) + brand-resolver.mjs, com chave derivada do
ORIGIN_DOMAIN do worker.
Não existe fork do front-web-base — toda marca em produção é um diretório
de override dentro do próprio template. As marcas cassino-bet-br e
vera-bet-br foram removidas do base em 2026-07-28 e não têm mais override nem
ambiente de deploy.
Pontos rápidos de customização por marca:
app/config/theme/colors.ts— paleta de coresapp/config/layout/composition.ts— composição do layout (slots e variantes)app/config/routes/paths.ts— customização de URL paths
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).
Ú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/<domínio>/<arquivo>.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/*
app/config/ é organizada em subpastas semânticas por domínio (22 hoje —
consulte app/config/ pra lista viva). Imports usam o path completo
~/config/<dir>/X, sem sufixo .config: ~/config/theme/colors,
~/config/widgets/sidebar-buttons, ~/config/layout/composition.
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.
:::danger Override substitui o arquivo INTEIRO
A substituição é file-replacement, nunca deep-merge. Ao adicionar um campo
novo a um config que tem overrides (features.ts, theme/colors.ts, …), é
obrigatório propagar o campo pra todos os overrides existentes — senão aquela
marca recebe undefined.
:::
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 | O documento SSR é auth-agnóstico: a store hidrata client-side a partir do cookie is_authenticated + /api/auth/profile. É isso que torna o HTML público compartilhável entre usuários (e portanto cacheável). Ver Fluxo de autenticação. |
| Uma store Zustand por domínio | Evita store monolítica e facilita code-splitting. Auth/usuário/wallet ficam numa store do core (useAccountsStore), não no base. |
| Modais controlados por Zustand | useAuthModalStore (app/store/authModal.ts). Qualquer componente abre/fecha. |
| EnvProvider separado do BrandProvider | clientEnv precisa existir antes de qualquer chamada client-side. Não carrega API_BASE_URL — o host do BFF nunca chega ao browser; o client só fala com rotas same-origin /api/*. |
Marca = overrides/<brand-key>/ no mesmo repo | File-replacement resolvido em build time. Sem repo por marca. |
| 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. |