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

16. Events catalog (300+ events, 29 namespaces)

Sistema 100% orientado a eventos no shape bigtec (PostHog/Mixpanel/Amplitude). Source: evian-bet-frontend/src/lib/events.ts. Pipeline: track()POST /api/events/track → Queue igaming-events → Consumer → D1 events_recent_90d + Analytics Engine.

Total namespaces
29
Total event types
300+
Sink writes/s capacity
5M

Universal envelope

Toda chamada de emit.{namespace}.{action}({data}) adiciona automaticamente:

{
  event_type: 'bet.placed',
  ts_client: 1716922800000,
  session_id: 'sess_uuid',
  device_id: 'dev_uuid',
  player_id: 'plr_uuid' | null,
  page_url, page_path, referrer,
  user_agent_hash, viewport,
  utm_source, utm_medium, utm_campaign, utm_content, utm_term,
  // ↓ added server-side from CF
  ip_country, ip_city, ip_region, ip_asn, ip_isp,
  // ↓ event-specific payload
  data: { game_id, bet_id, amount, currency, ... }
}

29 Namespaces

session
start, end, resume, idle, visibility_hidden/visible, focus_lost/gained, before_unload (9)
page
view, leave, scroll_depth, hash_change, route_change, back_button, deeplink, not_found (8)
attribution
first_touch, last_touch, affiliate_link, organic, direct (5)
performance
LCP, FCP, CLS, INP, TTFB, slow page/api/ws, memory, long_task (10)
device
new, change, orientation, resize, theme, language (6)
signup
modal_open, field_focused/blurred/changed, validations, abandoned (17)
auth
login, 2FA, password_reset, email_verified, session_expired, rapid_attempts (19)
funnel
signup→deposit→bet→withdraw lifecycle, generic step_entered/completed (13)
game
discovery (filter/sort/search), thumbnail impressions/clicks, launch, favorites (23)
bet
draft amount + presets, auto-cashout, cashout micro (hover/click/decision_time), big_win, unusual_size (24)
wallet
deposit funnel (method→qr→complete), withdrawal funnel (kyc_block→confirm), currency switch, exchange (26)
social
feed/post lifecycle, follow/unfollow, DM (sent/delivered/read/block) (19)
kyc / rg
page_view, sumsub, documents, approved/rejected, limits, self-exclusion, reality_check (18)
bonus
offer impressions, claim, wagering milestones (25/50/75/90%), completed, forfeit (11)
vip
tier_up/down, perks, points earned/redeemed, cashback (9)
referral
link_copied, share, attribution, conversion (cpa/revshare), payout (7)
push
banner, permission lifecycle, subscribed/unsubscribed, received/displayed/clicked (15)
pwa
install, opened, standalone, offline_loaded, update_available/applied (12)
comms
email/sms/whatsapp lifecycle (sent→delivered→opened→clicked→bounced/unsubscribed), OTP (17)
chat
widget, ticket lifecycle, message, agent_assigned, satisfaction (9)
experiment
assigned, viewed, conversion, feature_flag_evaluated, forced_variant (5)
search
performed, result_clicked, no_results, suggestion (4)
form
started, submitted, abandoned, validation_error, server_error, autofilled (6)
fraud_signal
vpn/proxy/tor, emulator, ip/device/clock_mismatch, multi_account, velocity, geo (11)
error
api, javascript, ws, resource, unhandled_promise, payment/casino_provider, boundary (8)
ws
connecting/connected/reconnected/disconnected, msg sent/received, silence (8)
network
online/offline, connection_change (4G→2G), save_data_enabled (4)
cta
hero, banner impression/click/dismiss, fab, rage_click, dead_click, copy (10)
account
data_export, deletion, consent_changed, profile_edited, avatar, preference (9)

Eventos de uso

import { emit } from '@/lib/events'

emit.bet.placed({ game_id: 'crash', bet_id, amount: 50, currency: 'BRL', via: 'ws' })
emit.funnel.firstDepositCompleted({ time_to_first_deposit_sec: 142, amount_minor: 5000, currency: 'BRL' })
emit.fraud_signal.vpnDetected({ provider: 'NordVPN' })
emit.experiment.assigned({ experiment: 'new-cashout-button-color', variant: 'green' })
emit.performance.webVitalLcp({ value_ms: 2400, path: '/cassino', rating: 'good' })

17. Filters & dimensions (analytics)

Toda query Reports/BI no BO suporta esses filtros. Documenta o esquema de cubo OLAP.

Dimensões (filtros)

CategoriaDimensõesOrigem
Identidadetenant_id, player_id, session_id, device_id, anonymous_idEnvelope universal
Geoip_country, ip_region, ip_city, ip_postal_code, ip_timezone, ip_asn, ip_ispCF cf-data headers
Dispositivodevice_type (desktop/mobile/tablet), os, browser, browser_version, viewport_w, viewport_h, pixel_ratioUA parsing + JS
Attributionutm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer, landing_page, gclid, fbclid, affiliate_codeURL params + localStorage first_touch
Perfilvip_level, kyc_level, language, primary_currency, registration_date_bucket, age_bucket, gender, segment_id[]players table join
Tempohour, day, week, month, quarter, year, day_of_week, hour_of_dayDerivado de ts_client / ts_server
Gamegame_id, provider_id, category, subcategory, currency_played, bet_size_bucket, rtp_bucket, volatilitycasino_games_catalog join
Comportamentosession_count_bucket, ltv_bucket, days_since_last_session, days_since_first_deposit, churn_risk_scoreComputed
Experimentoexperiment_assignments[] (active variants)Workers experiment binding
A/B segmentscustom_segment[] (e.g. "high_value", "at_risk", "vip_diamond")Computed via segmentation worker

Métricas (aggregations)

MétricaCálculoWhere
DAU/WAU/MAUCOUNT(DISTINCT player_id) by day/week/monthevents_recent
SessionsCOUNT(DISTINCT session_id)events_recent
Avg session durationAVG(session.end.duration_ms)events session.end
Page views per sessionSUM(page.view) / sessionsevents page.view
Conversion rate signup→depositfunnel.first_deposit_completed / funnel.signup_completedevents funnel.*
D1/D7/D30/D90 retentionCohort matrix: % returning N days after signupevents signup.completed + session.start
LTVSUM(deposits) - SUM(withdrawals) per playerledger_entries
ARPU/ARPPUNGR / total_users e NGR / paying_usersledger_entries
GGRSUM(bet.amount) - SUM(win_amount)events bet.settled + bet.placed
NGRGGR - bonuses - feescomputed
RTP realSUM(win) / SUM(bet)events bet.*
Churn rate% players inactive > 30dlast_login_at
Latency P50/P90/P99Histogram percentiles em events performance.api_latency_slowAnalytics Engine
Push CTRnotification_clicked / notification_displayedevents push.*
Email open rateemail_opened / email_deliveredevents comms.*
Bet decision timeHistogram bet.cashout_clicked.decision_time_msAnalytics Engine

Exemplos SQL (Analytics Engine)

-- DAU últimos 30d por country
SELECT
  blob1 AS country,
  formatDateTime(timestamp, '%Y-%m-%d') AS day,
  COUNT(DISTINCT blob2) AS dau
FROM events_analytics
WHERE timestamp > now() - INTERVAL '30' DAY
  AND blob3 = 'session.start'
GROUP BY country, day
ORDER BY day DESC, dau DESC;

-- Conversion funnel signup → first deposit por UTM source
SELECT
  blob4 AS utm_source,
  countIf(blob3 = 'funnel.signup_completed') AS signups,
  countIf(blob3 = 'funnel.first_deposit_completed') AS first_deposits,
  countIf(blob3 = 'funnel.first_deposit_completed') / countIf(blob3 = 'funnel.signup_completed') AS conv_rate
FROM events_analytics
WHERE timestamp > now() - INTERVAL '7' DAY
GROUP BY utm_source
ORDER BY signups DESC;

-- Bet decision time histogram (P50/P90/P99)
SELECT
  quantile(0.5)(double1) AS p50_ms,
  quantile(0.9)(double1) AS p90_ms,
  quantile(0.99)(double1) AS p99_ms
FROM events_analytics
WHERE blob3 = 'bet.cashout_clicked'
  AND timestamp > now() - INTERVAL '24' HOUR;

18. Backoffice inventory (12 BOs + System)

BONomeStatusURL
BO-01Player Management (360°)● Live/bo/player · 8 tabs · 22 endpoints
BO-02Finance● Live (refinement pending)/bo/finance · /providers
BO-03Bonus Management● Live/bo/bonus · catálogo + analytics + grant manual
BO-04Marketing / CRM○ StubK-021 (backlog)
BO-05Anti-Fraud● Live (basic)/bo/fraud · 6 tabs
BO-06Risk Management○ StubK-022 (backlog)
BO-07KYC Operations● PartialK-023 (expansão pendente)
BO-08Affiliate Management○ StubK-024 (backlog)
BO-09Content (Games/Banners/CMS/Tema)● Live/bo/content
BO-10Reports & BI● Live/bo/reports · 12 KPIs + cohort + funnel
BO-11Compliance & Regulatory Reports○ StubK-025 (backlog)
BO-12Operations Dashboard○ StubK-026 (backlog)
System (super_admin)
SYSOverview / Admin Users / Audit● Live/system/{overview,users,audit}
SYSTenants Management● Live/system/tenants + editor com 11 tabs
SYSAnalytics & Realtime● Live/system/analytics, /realtime
SYSCMS● Live/system/cms
SYSStatus & Monitor● Live/system/status · 19 services monitored
SYSWAF & Security● Live/system/waf · zone rules + apex middlewares + per-tenant
SYSPayment Providers● Live/system/payment-providers · 7 providers no catálogo
SYSCasino Providers● Live/system/casino-providers · 8 providers

19. Data model — D1 tabelas críticas

erDiagram TENANTS ||--o{ PLAYERS : has TENANTS ||--o{ TENANT_PAYMENT_CONFIG : has TENANTS ||--o{ TENANT_CASINO_CONFIG : has PLAYERS ||--o{ WALLETS : owns PLAYERS ||--o{ LEDGER_ENTRIES : has PLAYERS ||--o{ TRANSACTIONS : has PLAYERS ||--o{ PLAYER_BONUSES : has PLAYERS ||--o{ CASINO_SESSIONS : has PLAYERS ||--o{ PLAYER_LOCKS : has PLAYERS ||--o{ PLAYER_NOTES : has PLAYERS ||--o{ KYC_STATUS : has PAYMENT_PROVIDERS ||--o{ TENANT_PAYMENT_CONFIG : configured_in PAYMENT_PROVIDERS ||--o{ PAYMENT_ATTEMPTS : tracks CASINO_PROVIDERS ||--o{ CASINO_GAMES_CATALOG : produces CASINO_PROVIDERS ||--o{ TENANT_CASINO_CONFIG : configured_in CASINO_PROVIDERS ||--o{ CASINO_SESSIONS : hosts TENANTS { string id PK string brand_name string status string primary_domain string cloudflare_zone_id int config_version } PLAYERS { string id PK string tenant_id FK string email_hash string display_name int kyc_level int vip_level string status } LEDGER_ENTRIES { string id PK string player_id FK string type string direction int amount_minor string currency string ref_type string ref_id int archived } PAYMENT_PROVIDERS { string id PK string type json capabilities json currencies json countries } TENANT_PAYMENT_CONFIG { string tenant_id FK string provider_id FK int enabled int priority int weight string failover_to }

D1 tables: 37 em platform-main + 8 em player-ledger + 11 em cactus-brands-s1/s2 + 4 em cactus-ledger-s1 + 5 em cactus-kanban = ~65 tables distribuídas por sharding.

20. Withdrawal Rules Engine

Engine de regras automáticas per (tenant × payment_method × currency). Avalia rules por priority, primeira match decide: auto_approve | require_review | block | delay. Default safe = require_review.

Schema (D1 platform-main)
  • withdrawal_rules — id, tenant_id, payment_method, currency, priority, action, min_kyc_level, min_account_age, max_velocity_24h, required_flags JSON
  • withdrawal_decisions — log per request (reviewer_id, action_taken, reasons)
Endpoint engine
POST /api/withdrawal/evaluate
Header: X-Internal-Secret
Body: { tenant_id, player_id, payment_method, currency, amount_minor }
→ { action, action_taken, matched_rule, reasons[], context }

BO UI: /bo/finance/withdrawal-rules · 7 rules seed evian_bet

21. Payment Failover Routing

Chain ordenada per (tenant × payment_method) com triggers granulares + retry policy + circuit breaker. Se Stripe falhar com server_error, tenta MercadoPago. Se cartão declined (terminal error), aborta sem tentar próximo.

Schema (D1 platform-main)
  • payment_method_routing — tenant_id, payment_method, provider_chain JSON ordered, triggers JSON, retry_per_provider, retry_backoff_ms, retry_jitter_pct
  • payment_attempts (estendida) — chain_position, chain_total, parent_attempt_id, failover_reason
Triggers exemplo PIX
{
  "on": ["timeout","server_error","provider_down","fraud_decline"],
  "skip": ["customer_decline","invalid_amount"]
}

on = failover dispara · skip = aborta terminal

BO UI: /bo/finance/failover · 3 tabs (Chains · Stats · Tentativas log) · 6 routings seed evian_bet

22. Credentials Vault (Secrets Central)

Metadata em D1 + valores reais em CONFIG_KV. Workers leem KV normalmente. BO super-admin pode listar/filtrar/rotacionar/audit. Reveal explícito logado em audit trail (PCI-DSS compliant).

11 namespaces seeded
  • cloudflare (api_token, account_id, 2× zone_id)
  • internal (X-Internal-Secret)
  • push (VAPID public/private/subject)
  • comms (Resend, Twilio, Meta WhatsApp)
  • monitor (Telegram)
  • sentry (DSN frontend/workers)
  • payments (Stripe, MercadoPago, Coinbase, NowPayments)
  • casino (Pragmatic, Evolution, BGaming, Softswiss, Hacksaw)
  • kyc (Sumsub, Onfido)
  • fraud (MaxMind, IPQS)
  • marketing (Meta Pixel, Google Ads, TikTok)
Endpoints
GET /api/bo/secrets
GET /api/bo/secrets/:id  (com audit log)
POST /api/bo/secrets  (create/rotate)
POST /api/bo/secrets/:id/reveal  (1× + audit)
DELETE /api/bo/secrets/:id
GET /api/bo/secrets/health  (D1 vs KV consistency)

BO UI: /system/secrets super-admin only

23. KYC Operations (BO-07)

Queue de submissões KYC com priority. Admin revisa docs, aprova (upgrade tier automático), rejeita (com motivo estruturado em 8 reasons) ou pede mais info. Tudo audit-logged.

Schema
  • kyc_submissions — tier_requested, status, full_name, doc fields, R2 keys (selfie/doc_front/doc_back/proof_of_address), ocr_data, risk_signals, reviewer_id, reject_reason
  • kyc_review_actions — audit log per action
Workflow
  1. 1. Player submete docs em /conta/kyc
  2. 2. Backend cria submission em pending
  3. 3. Admin vê queue priorizada em /bo/kyc-ops
  4. 4. Approve → kyc_level upgrade · Reject → motivo · More info → msg pro player

BO UI: /bo/kyc-ops · SLA tracking · 8 reject reasons

24. Push Campaigns BO

Cria campanhas com template + segment + agendamento. Stats real-time: subs ativas, delivered, clicked, CTR%.

BO UI: /bo/push-campaigns · Worker igaming-push dispatch · 5 templates seed (welcome_bonus, deposit_confirmed, withdrawal_processed, crash_round_alert, weekly_cashback)

25. Funnel Events Automation

Cron diário 02:30 UTC roda runFunnelTracker per tenant. Detecta marcos lifecycle (first_deposit, first_bet, first_withdrawal, churned_7d/30d), emit events EVENTS_QUEUE + snapshot em funnel_daily_snapshots pra dashboard histórico.

BO UI: /bo/funnel · Funnel viz horizontal + Churn cards 7d/30d + Daily breakdown table

26. Originals (Cactus)

7 jogos originais provably-fair (server seed HMAC-SHA256 + client seed + nonce).

Crash
WS canvas 60fps · 4 bets simultâneos · auto-cashout · live lobby
Mines
5×5 grid · 1-24 minas configuráveis · pop animation
Tower
Easy/Med/Hard/Expert · climbing multiplier
Plinko
Canvas physics · 8/12/16 rows × Low/Med/High = 9 modes · até 1000x
Dice
Slider 0-100 over/under · multiplier=99/winChance%
Limbo
Target multiplier · cauda longa até 1Mx · 1% house edge
Wheel
10/20/30 segments × 3 risk levels · SVG rotation
+ Roadmap
HiLo, Roulette, Coinflip, Diamonds, Video Poker, Baccarat

Status backend: Crash/Mines/Tower com HMAC server-side. Plinko/Dice/Limbo/Wheel ainda usam Math.random client-side — k-P0-01 pendente.

27. Minigames CRM

Mini-games de bônus integrados em journeys (boas-vindas, pós-depósito, KYC tier-up, push retention). Backend determina prize, UI anima reveal.

ScratchCard (/components/minigames/ScratchCard.tsx)
  • Canvas raspável (pointer + destination-out)
  • Auto-reveal em 60% raspado + confetti
  • Claim flow → POST endpoint backend
SpinWheelBonus (/components/minigames/SpinWheelBonus.tsx)
  • SVG wheel 8 segments default
  • Server-determined prize_index
  • 3.5s cubic-bezier rotation animation

Roadmap: Claw machine, Baú/Treasure Chest, Slot 1-line, Memory match-3, Dart throw.

28. WordPress-Multisite Parity

Cada tenant edita praticamente tudo do site dele via BO — inspirado em WordPress Multisite. Score atual: 11✅ / 3🟡 / 2🔴.

FeatureStatusEdita em
Identidade (logo/favicon/OG)/system/tenants/:id (Geral+Theme)
Tema (cores, fonts, custom CSS)Theme tab
Conteúdo páginas (Termos/Privacy/FAQ)/system/cms (cms_pages)
Menus/navegaçãoNavegação tab (top_nav + footer_columns)
Users/roles (multi-brand admin)/system/users com scope[]
Features (Crash/Mines/Tower/VIP/Bonus)Features tab + feature_flags
Payment methods/bo/finance/providers
Game catalog/bo/content Games tab
Promotions/banners/bo/content BannersPanel
Email templates/bo/marketing/email-templates
Legal pages dinâmicasCMS bridge live (k-P0-02 entregue)
Settings (currency/country/timezone)🟡MarketTab — falta timezone explícito
SEO per page🟡Global em tenant_config[seo]; per-page WIP
Custom domain automation🟡Campo no editor; CF API integration #158
Widgets sidebar/footer🔴Sem demanda real
i18n (multi-language)🔴react-i18next pendente (#157)

29. Architecture Decision Records

ADR-001: WalletDO single-writer vs D1 puro

Decisão: Durable Object per player como single-writer pra wallet ops.
Motivo: CF D1 não tem locks/transactions multi-row consistentes. Race conditions em bets simultâneos. DO garante linearizable writes per player.
Trade-off: +latency 5-20ms vs D1 direto, mas zero double-spend.
Sync: Queue → D1 batcher (eventual consistency) pra queries históricas.

ADR-002: D1 sharding strategy

Decisão: CRC32(tenant_id) % N pra brands, CRC32(player_id) % M pra ledger. Shard map em CONFIG_KV.
Motivo: D1 single-region 10GB limit. Brands escalonáveis horizontalmente.
Cutover: tenant_assignments['evian_bet']='s0_legacy' permanece até precisar mover. Novos brands começam em s1/s2.

ADR-003: Iceberg JSONL.gz em vez de Parquet

Decisão: R2 hot archive em JSONL gzipped (não Parquet).
Motivo: Workers runtime não tem libs Parquet writer (Apache Arrow é heavy). JSONL.gz funciona com CompressionStream native, layout Iceberg-compatible.
Migração futura: DuckDB-WASM pode ler ambos.

ADR-004: Service binding vs HTTP fetch entre Workers

Decisão: Service bindings ([[services]]) obrigatórios pra Worker→Worker.
Motivo: CF erro 1042 "loopback" quando Worker chama via HTTP fetch o próprio domínio workers.dev. Service binding contorna isso e tem 0ms overhead extra.
Bug histórico: bo-ops tentou fetch wallet-do via workers.dev URL → 404 "Canceled". Fix: WALLET_DO_SERVICE binding.

ADR-005: Single source of truth currencies

Decisão: tenant_config[market.supported_currencies] é fonte única. Workers consultam dali.
Motivo: Player não pode ter wallets diferentes do tenant. Adicionar moeda no admin = endpoint POST /api/admin/wallet/sync-tenant-currencies propaga pra todos players (idempotente).

ADR-006: Provably-fair HMAC-SHA256

Decisão: Server gera server_seed aleatório 32 bytes, publica SHA256(server_seed) antes do round. Player escolhe client_seed. Outcome = HMAC-SHA256(server_seed, client_seed:nonce). Após round, server_seed é revelado pra verificação.
Compliance: Padrão BC.Game/Stake auditável.

30. Onboarding novo brand (passo-a-passo)

  1. Criar tenant: /system/tenants → "Novo cassino" → 5 steps wizard (id/dominios/moeda/licença/confirmar)
  2. Configurar tema: Aba Tema → cores + logo + favicon + (opcional) font_family + custom_css
  3. Definir navegação: Aba Navegação → top_nav + footer_columns (ou carregar defaults)
  4. Habilitar features: Aba Features → toggles per jogo + payment_methods + casino_providers
  5. Currencies: Aba Currencies — markar moedas suportadas (auto-init wallets pra signups)
  6. Payment routing: /bo/finance/providers — ativar PSPs + /bo/finance/failover — chain ordenada
  7. Casino providers: /system/casino-providers — ativar provs globais + per-tenant game allowlist
  8. Withdrawal rules: /bo/finance/withdrawal-rules — regras automáticas approve/review/block
  9. CMS pages: /system/cms — criar Termos/Privacidade/Sobre/FAQ/Jogo Responsável em pt-BR + locale extras
  10. Email templates: /bo/marketing/email-templates — customizar welcome, deposit_confirmed etc
  11. Push templates + campaigns: /bo/push-campaigns
  12. Secrets: /system/secrets — preencher API keys (Stripe, MercadoPago, Sumsub, etc)
  13. WAF + geo-block: /system/waf — restricted_countries + Tor/VPN flags
  14. Custom domain: No CF Pages, adicionar domain custom + CNAME (manual hoje, #158 automatize)
  15. Validate + ativar: Wizard step 5 "Validate" + status=active

Tempo estimado: ~2h pra brand novo após PSPs/license aprovados.