Cactus v10 — Architecture

Cactus v10 — Architecture Reference

Plataforma multi-tenant iGaming 100% Cloudflare edge. Target: 100 brands, 30M usuários, 400M tx/dia.

Workers ativos
33+
D1 databases
5
Durable Objects
3
Queues
10+

1. Topologia global

Tudo no edge Cloudflare. Apex Worker é o único entry-point externo; faz service bindings pra todos os Workers especializados.

graph TB subgraph "Edge (player)" Browser[Player Browser/PWA] --> CFNET[Cloudflare CDN + WAF] end CFNET --> Apex[igaming-apex Worker] subgraph "Apex Middlewares" Apex --> RL[Rate Limit KV] Apex --> Geo[Geo Block per-tenant] Apex --> Tenant[Tenant detection] end Apex -->|service bindings| Auth[igaming-auth] Apex --> Wallet[igaming-wallet] Apex --> Games[igaming-games + CrashDO] Apex --> Casino[igaming-casino-orchestrator] Apex --> Payments[igaming-payments] Apex --> Push[igaming-push] Apex --> Comms[igaming-comms] Apex --> Chat[igaming-livechat + ChatDO] Apex --> Social[igaming-social] Apex --> Reports[igaming-reports] Apex --> KYC[igaming-kyc] Apex --> Bonus[igaming-bonus] Apex --> VIP[igaming-vip] Apex --> RG[igaming-rg] Apex --> Affiliate[igaming-affiliate] Apex --> WalletDO[igaming-wallet-do Worker] Apex --> GDPR[igaming-gdpr] subgraph "Storage Layer" D1Global[(D1 platform-main)] D1Ledger[(D1 player-ledger)] D1Brands1[(D1 cactus-brands-s1)] D1Brands2[(D1 cactus-brands-s2)] D1LedgerS1[(D1 cactus-ledger-s1)] R2Iceberg[(R2 cactus-iceberg)] R2Exports[(R2 cactus-player-exports)] KV[(CONFIG_KV + SESSIONS_KV + RATELIM_KV)] end Wallet --> D1Ledger WalletDO -.-> D1LedgerS1 Games --> D1Ledger Auth --> D1Global subgraph "Async Pipeline" Q1[Queue igaming-events] Q2[Queue igaming-d1-writes] Q3[Queue igaming-payments-events] Batcher[igaming-d1-batcher] Archiver[igaming-archiver cron] Consumer[analytics-consumer] AE[Analytics Engine] end WalletDO --> Q2 --> Batcher --> D1Ledger Wallet --> Q1 Games --> Q1 Q1 --> Consumer --> AE Q1 --> D1Global Payments --> Q3 Archiver -.->|nightly 03:00 UTC| R2Iceberg style Apex fill:#5865f2,color:#fff style WalletDO fill:#22c55e,color:#000 style R2Iceberg fill:#f59e0b,color:#000

2. Workers index

Inventário completo. URLs internas via service binding; URLs externas via apex proxy.

WorkerFunçãoBindingsEndpoints públicos
igaming-apexEntry point, WAF, rate limit, geo, proxy+15 service bindings, CONFIG_KV, RATELIM_KVevian.bet/*
igaming-authJWT, login, OTP, password resetD1, CONFIG_KV, SESSIONS_KV, EVENTS_QUEUE/api/auth/*
igaming-walletWallets, balances, transfersD1, EVENTS_QUEUE/api/wallet/*
igaming-wallet-doWalletDO single-writer per playerWALLET_DO (DO), SYNC_QUEUE, EVENTS_QUEUE/wallet-do/{id}/{op}
igaming-gamesCrash/Mines/Tower + CrashRoundDOCRASH_DO, WALLET_SERVICE, EVENTS_QUEUE/api/games/*
igaming-casino-orchestratorCasino aggregator frameworkD1, WALLET_SERVICE, EVENTS_QUEUE/api/casino/*
igaming-paymentsPayment adapter frameworkD1, WALLET_SERVICE, PAYMENT_EVENTS_QUEUE/api/payments/*
igaming-pushVAPID push notificationsD1, CONFIG_KV/push/*
igaming-commsEmail (Resend) + SMS (Twilio) + WhatsAppD1, CONFIG_KV, SESSIONS_KV/comms/*
igaming-livechatWebSocket chat + DM + ChatDOCHAT_DO (DO), D1/api/chat/*
igaming-socialFeed, posts, likes, commentsD1, EVENTS_QUEUE/api/social/*
igaming-reportsAnalytics, BI reports MCP-shapeD1, AE (Analytics Engine)/api/reports/*
igaming-kycKYC tiers via SumsubD1, R2 (kyc docs), EVENTS_QUEUE/api/kyc/*
igaming-bonusBonus engine + wagering trackerD1, WALLET_SERVICE, EVENTS_QUEUE/api/bonus/*
igaming-vipVIP tiers + loyalty pointsD1, EVENTS_QUEUE/api/vip/*
igaming-rgResponsible Gaming limitsD1, EVENTS_QUEUE/api/rg/*
igaming-affiliateReferral + CPA + RevShareD1, EVENTS_QUEUE/api/affiliate/*
igaming-gdprData export + right-to-deletionD1, R2 exports, cron daily/api/me/data-export
igaming-backofficeAdmin auth + tenants + auditD1, CONFIG_KV/api/bo/*
igaming-monitorStatus page + uptime cronD1, CONFIG_KV, cron 2min/monitor/status
igaming-d1-batcherQueue → batched INSERT D15x D1 (all shards), CONFIG_KVqueue consumer
igaming-archiverD1 hot → R2 Iceberg cron5x D1, R2, cron 03:00 UTC/archive/*
igaming-do-backupWalletDO snapshot weeklyWALLET_DO_SERVICE, R2, cron SUN 04:00/backup/*
igaming-analytics-consumerQueue events → D1 + AED1, AEqueue consumer
igaming-kanban-apiKanban backend (boards/cards)D1, CONFIG_KV/api/kanban/*

3. Request flow — apex middleware stack

Toda request HTTP entra pelo apex e passa por 4 camadas antes de chegar no Worker downstream.

sequenceDiagram participant Client participant CF as Cloudflare CDN+WAF participant Apex as igaming-apex Worker participant DS as Downstream Worker participant KV as CONFIG_KV / RATELIM_KV Client->>CF: HTTPS request CF->>CF: Zone WAF rules
(scanner UA, threat_score) CF->>Apex: forward Apex->>KV: rate limit check (KV-backed sliding window) alt rate limit excedido KV-->>Apex: 429 Apex-->>Client: 429 Too Many Requests end Apex->>KV: tenant lookup (hostname → tenant_id) Apex->>KV: geo-block check (CF-IPCountry vs tenant.restricted_countries) alt geo blocked Apex-->>Client: 451 Unavailable For Legal Reasons end Apex->>Apex: enrich headers (X-CF-Country/City/ASN) Apex->>DS: service binding fetch (proxyReq) DS-->>Apex: response Apex-->>Client: response + CORS headers

4. Crash WebSocket — bet + cashout em ~30ms

Player abre WS persistente. Bet/cashout viajam via WS já aberto (sem TCP handshake) com ACK direto pro socket. Wallet credit em waitUntil.

sequenceDiagram participant P as Player Browser participant A as igaming-apex participant G as igaming-games participant DO as CrashRoundDO participant W as igaming-wallet P->>A: GET /api/games/crash/connect?token=JWT (WS Upgrade) A->>G: forward WS G->>G: getPlayer(req) → decode JWT G->>DO: stub.fetch(ws/?pid=PLAYER_ID) DO->>P: state + history (initial) Note over DO,P: WS established, tag=PLAYER_ID loop Round em "running" (a cada 200ms) DO->>P: {type: tick, multiplier, elapsed_ms} end P->>DO: {type: bet, client_id, amount, currency} DO->>DO: validate phase, amount, pid (not "anon") DO->>W: walletDebit(pid, amount) via WALLET_SERVICE W-->>DO: success DO->>DO: _doBet → INSERT bets, _bcast bet_placed DO->>P: {type: bet_ack, client_id, success: true, bet_id} Note over P: UI mostra bet ativo IMEDIATO P->>DO: {type: cashout, client_id, bet_id} DO->>DO: _doCashout síncrono → calcula multiplier no instant DO->>P: {type: cashout_ack, multiplier, win_amount} Note over P: UI mostra "saiu em 2.34x" sem esperar wallet DO->>W: walletCredit (em ctx.waitUntil, async)

5. WalletDO sync pipeline

Single-writer per player. SQLite local no DO (consistency forte). Sync periódico pra D1 via queue.

graph LR Player[Player] -->|/bet /credit| DO[WalletDO instance] DO -->|local SQLite| LocalDB[(SQLite storage)] DO -->|alarm 60s OR 100 ops dirty| Sync[forceSync] Sync -->|sendBatch| Q[Queue igaming-d1-writes] Q --> Batcher[igaming-d1-batcher] Batcher -->|INSERT OR IGNORE batch| D1[(D1 LEDGER_DB)] cron[Cron weekly SUN 04:00] -.->|do-backup| Backup[igaming-do-backup] Backup -->|service binding| DO Backup -->|gzip + put| R2[(R2 cactus-iceberg/wallet-do/)] style DO fill:#22c55e,color:#000 style D1 fill:#5865f2,color:#fff style R2 fill:#f59e0b,color:#000

6. Payment routing engine

Adapter pattern + routing por priority+weight+circuit breaker+failover. 7 providers no catálogo (mock, pix-direct, stripe, mercadopago, btag, coinbase, nowpayments).

flowchart TD Start([Player POST /api/payments/deposit]) --> Q1{tenant_payment_config} Q1 -->|filter currency+country+method| Cand[Candidates list] Cand -->|ORDER BY priority| Sorted[Sorted by priority] Sorted -->|same priority?| AB[Apply weight A/B] AB --> CB{Circuit Breaker check} CB -->|open| Skip[Skip provider] Skip --> Next{Next candidate?} Next -->|yes| AB Next -->|no| Fail([Return no_provider_available]) CB -->|closed| Call[adapter.createDeposit] Call -->|success| Done([Return instructions+qr+url]) Call -->|fail| FB{failover_to set?} FB -->|yes| Explicit[Use explicit failover] FB -->|no| Next Explicit --> CB Done -.->|webhook from PSP| Webhook[POST /webhook/:provider] Webhook --> Normalize[adapter.parseWebhook] Normalize --> Queue[(igaming-payments-events)] Queue --> Consumer[Consumer] Consumer -->|deposit.confirmed| WC[WALLET_SERVICE credit] style Done fill:#22c55e,color:#000 style Fail fill:#ef4444,color:#fff style CB fill:#f59e0b,color:#000

7. Casino launch + callback

Player lança game → orchestrator gera session → provider iframe URL. Provider chama nosso wallet via callback HMAC.

sequenceDiagram participant Player participant Orchestrator as igaming-casino-orchestrator participant Adapter as Pragmatic/SoftSwiss adapter participant Provider as Provider API participant Wallet as igaming-wallet Player->>Orchestrator: POST /api/casino/launch/{game_id} Orchestrator->>Orchestrator: lookup tenant_casino_config (enabled?) Orchestrator->>Adapter: launchGame({player, game, currency}) Adapter->>Provider: API call (auth + token) Provider-->>Adapter: iframe_url + session_ref Adapter-->>Orchestrator: result Orchestrator->>Orchestrator: INSERT casino_sessions Orchestrator-->>Player: {iframe_url} Player->>Provider: iframe loads Note over Player,Provider: Player joga Provider->>Orchestrator: POST /api/casino/callback/{provider} (bet/win/refund) Orchestrator->>Adapter: handleCallback (validate HMAC + normalize) Adapter-->>Orchestrator: {type, session_token, amount, currency} Orchestrator->>Wallet: debit/credit (via WALLET_SERVICE) Wallet-->>Orchestrator: balance Orchestrator-->>Provider: adapter.formatCallbackResponse

8. D1 sharding strategy

Tenant_id → brand shard. Player_id → ledger shard via crc32(). Mapeamento em CONFIG_KV[arch:shard_map] com cache 60s.

graph TD ShardMap{{CONFIG_KV[arch:shard_map]}} Router[lib/d1router.js] ShardMap --> Router subgraph "Brand shards (per-tenant data)" B0[platform-main
s0_legacy
evian_bet] B1[cactus-brands-s1
brands 1-20] B2[cactus-brands-s2
brands 21-40] end subgraph "Ledger shards (per-player_id)" L0[player-ledger
s0_legacy
crc32 mod 2 = 0] L1[cactus-ledger-s1
s1
crc32 mod 2 = 1] end subgraph "Global (small tables)" G1[(tenants)] G2[(admin_users)] G3[(payment_providers)] G4[(casino_providers)] end Router -->|getBrandDb tenant_id| B0 Router --> B1 Router --> B2 Router -->|getLedgerDbForPlayer pid| L0 Router --> L1 Router -->|getGlobalDb| G1

9. Archive D1 hot → R2 Iceberg (nightly)

Cron 03:00 UTC. Move rows com archived=0 AND created_at < 90d pra R2 em layout Iceberg-compatible (JSONL.gz). DELETE com grace 10d.

graph TB Cron[Cron 03:00 UTC] --> Archiver[igaming-archiver] Archiver --> Foreach{Pra cada rule} Foreach -->|bets_recent| Shards1[Lista brand shards] Foreach -->|events_recent| Shards1 Foreach -->|ledger_entries| Shards2[Lista ledger shards] Foreach -->|transactions| Shards2 Shards1 --> Select[SELECT * WHERE archived=0 AND created_at < 90d LIMIT 5000] Shards2 --> Select Select --> Group[Group by tenant/year/month/day] Group --> Gzip[JSONL gzip via CompressionStream] Gzip --> R2Put[R2 put
warehouse/tenant=X/database=Y/table=Z/year/month/day/part-uuid.jsonl.gz] R2Put --> Snapshot[Iceberg snapshot manifest
_metadata/snapshot-ts-shard.json] Snapshot --> Mark[UPDATE archived=1] Mark --> Delete[DELETE WHERE archived=1 AND created_at < 100d] style Cron fill:#f59e0b,color:#000 style R2Put fill:#22c55e,color:#000

10. GDPR data export self-service

Player baixa ZIP JSON.gz com TODOS seus dados em <30s. Right-to-deletion com 14d cooldown.

sequenceDiagram participant Player participant GDPR as igaming-gdpr participant D1 participant R2 as R2 cactus-player-exports Player->>GDPR: POST /api/me/data-export GDPR->>D1: check existing job (rate limit 7d) GDPR->>D1: INSERT gdpr_export_jobs (status=pending) GDPR-->>Player: {job_id, status: pending} Note over GDPR: processExportJob (inline OR queue) GDPR->>D1: SELECT profile, wallets GDPR->>D1: SELECT ledger_entries (LIMIT 50000) GDPR->>D1: SELECT transactions, bonuses, kyc, casino_sessions, audit GDPR->>GDPR: JSON.stringify + CompressionStream gzip GDPR->>R2: put(exports/{player_id}/{job_id}.json.gz) GDPR->>D1: UPDATE status=ready, download_url, expires_at=+7d Player->>GDPR: GET /api/me/data-export/jobs/:id GDPR-->>Player: {status: ready, download_url} Player->>GDPR: GET /download/:id GDPR->>R2: get(key) R2-->>GDPR: stream GDPR-->>Player: ZIP file (Content-Encoding: gzip)

11. Push notifications flow (VAPID)

VAPID via Web Crypto (sem deps). Anonymous subscribe permitido (device_id link).

sequenceDiagram participant SW as Service Worker participant Browser participant Push as igaming-push participant CRM as Backoffice CRM participant Browser2 as Browser (online) Browser->>Push: GET /push/vapid → public key Browser->>Browser: registration.pushManager.subscribe(vapidKey) Browser->>Push: POST /push/subscribe {endpoint, keys} Push->>Push: INSERT push_subscriptions Note over CRM: Admin cria campaign no BO CRM->>Push: POST /push/admin/send {template_id, segment} Push->>Push: query subscriptions match segment loop pra cada sub Push->>Push: build VAPID JWT (ES256) Push->>Push: AES128-GCM encrypt payload Push->>SW: POST to subscription.endpoint end SW->>Browser: showNotification (mesmo offline) Browser->>Push: POST /push/track (click attribution)

12. WAF & Security layers

4 camadas de defesa. Per-tenant geo dinâmico via CONFIG_KV[tenant:X:config].restricted_countries.

CamadaImplementaçãoStatus
1. CF Zone WAF5 custom rules (UA scanner, threat_score, geo edge)✅ Active
2. Rate Limit Worker16 rules KV-backed (login 10/60s, withdraw 5/60s, OTP 3/5min, generic 600/min)✅ Active
3. Geo-block per-tenantRuntime check CF-IPCountry vs tenant.restricted_countries → HTTP 451✅ Active
4. CORS + Origin validationApex middleware bloqueia mutating requests com Origin inesperado✅ Active
5. Bot Fight ModeCF nativo + UA validation no apex✅ Active
6. Service Binding gatewayWorkers só falam entre si via service bindings (sem fetch HTTP loopback CF 1042)✅ Active
7. Circuit BreakersKV state machine per (provider×tenant), 5 falhas/60s → open 30s✅ Active

13. Events pipeline (200+ event types)

Toda ação importante emite event. Pipeline: Worker → Queue → Consumer → D1 + Analytics Engine.

graph LR W1[auth Worker] -->|emitEvent| Q[(Queue igaming-events)] W2[wallet Worker] --> Q W3[games Worker] --> Q W4[bonus Worker] --> Q W5[kyc Worker] --> Q W6[social/affiliate/etc] --> Q W7[Frontend /events/track] --> Q Q --> Consumer[analytics-consumer
batch 100 / 5s] Consumer --> AE[(Analytics Engine
5M writes/s capacity)] Consumer --> D1[(D1 events_recent_90d)] AE -->|SQL aggregate| Reports[igaming-reports] D1 -->|recent queries| Reports Reports -->|BI dashboards| BO[Backoffice /bo/reports]

14. Deployment + CI/CD

Tudo via wrangler. Pages Projects pra frontends. D1 + R2 + KV criados via CF API.

AssetTipoURL
evian-bet-frontendPagesstaging.evian.bet
cactus-v10-backofficePagesbo.cactus-v10.com
igaming-docsPagesdocs-new-stack.cactus-v10.com
cactus-kanbanPageskanban.cactus-v10.com
cactus-docs (esta página)Pagesdocs.cactus-v10.com

15. API endpoints index

Snapshot dos endpoints mais importantes. Lista completa em /api/admin/openapi (TODO).

Auth
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
POST /api/auth/refresh
POST /api/auth/forgot-password
POST /api/auth/reset-password
POST /api/auth/2fa/setup
GET  /api/auth/me
Wallet
GET  /api/wallet/balance
GET  /api/wallet/balances
GET  /api/wallet/history?limit=&offset=
POST /api/wallet/transfer
POST /api/wallet/exchange
Games (Crash/Mines/Tower)
WS   /api/games/crash/connect?token=JWT
       ws msg: {type: 'bet'|'cashout'|'ping'}
       ws ack: {type: 'bet_ack'|'cashout_ack'|'pong'}
GET  /api/games/crash/state
GET  /api/games/crash/history
POST /api/games/crash/bet (fallback)
POST /api/games/crash/cashout (fallback)
POST /api/games/mines/start
POST /api/games/mines/reveal
POST /api/games/tower/start
POST /api/games/tower/reveal
POST /api/games/verify (provably fair)
Payments
GET  /api/payments/methods?country=BR¤cy=BRL
POST /api/payments/deposit
POST /api/payments/withdrawal
POST /api/payments/webhook/:provider
GET  /api/admin/payments/providers
GET  /api/admin/payments/tenants/:tid/config
PATCH /api/admin/payments/tenants/:tid/config
POST /api/admin/payments/test/:provider
GET  /api/admin/payments/circuit-breakers
Casino
GET  /api/casino/games?category=slots&provider=&limit=
POST /api/casino/launch/:game_id
POST /api/casino/callback/:provider
GET  /api/admin/casino/providers
GET  /api/admin/casino/tenants/:tid/config
POST /api/admin/casino/sync/:provider
GDPR / Player rights
POST   /api/me/data-export
GET    /api/me/data-export/jobs
GET    /api/me/data-export/jobs/:id
GET    /api/me/data-export/download/:id
POST   /api/me/account-deletion
GET    /api/me/account-deletion
DELETE /api/me/account-deletion
Backoffice + System
GET  /api/bo/players?search=&status=
GET  /api/bo/players/:id/full
POST /api/bo/players/:id/lock
POST /api/bo/players/:id/credit
GET  /api/admin/system/tenants
PATCH /api/admin/system/tenants/:id
GET  /api/reports/overview
GET  /monitor/status (público)
GET  /api/kanban/boards
GET  /api/kanban/boards/:slug