Pular para o conteúdo principal

Cache architecture (técnico)

Visão sistêmica das camadas de cache na plataforma, com fluxos ponta-a-ponta, decisões arquiteturais e arquivos relevantes. Esta é a referência canônica pra entender por que cada cache existe, o que cada um cobre e como interagem.

Para a visão "pra quem não é técnico", veja Cache strategy. Para playbooks de operação, veja Cache operations.

Sumário

Camadas

[Browser]
├── HTTP cache (Cache-Control)
├── Service Worker (assets only — bypass se autenticado)
├── localStorage (api-client LocalStorageCache, TTL 60s)
└── sessionStorage (search, FTD flow, onboarding)

⇣ HTTPS

[Cloudflare Zone]
└── Cache Reserve (opcional, CT only) — não usado hoje



[Brand Worker — front-web-base]
├── Worker Middleware (workers/middleware.ts)
│ ├── SSR cache — namespace ssr:${BUILD_ID} (CF Cache API + KV)
│ ├── API cache — namespace api:${BUILD_ID} (CF Cache API + KV)
│ ├── SPA shell — namespace spa-shell:${BUILD_ID}
│ └── Assets — namespace assets:${BUILD_ID} (+ R2 archive fallback)

├── platform-cache engine (memory + CF Cache API + KV snapshot)
│ namespace: platform-cache-apcsrasj-v3
│ policy: PLATFORM_CACHE_POLICY_JSON

└── api-client (delegateCaching: true → no-op server-side)
└── server: BYPASS — every call goes to service-api
└── browser: LocalStorageCache TTL 60s

⇣ Service Binding API_SERVICE (no network)

[service-api Worker — Frankfurt, Smart Placement]
└── Per-path cache (CF Cache API + KV)
namespace: ${BUILD_KEY}-${GLOBAL_VERSION} (e.g. default-apcsrasj-v3)
policy: SERVICE_API_CACHE_POLICY_JSON



[BFF — api.bs2bet.com / bluetecconnect.com]

Fluxo end-to-end

Brand config (caso canônico)

User → CF Zone → Brand Worker SSR cache (cold)

platform-cache.engine.fetch("brandConfig", ...)
↓ tier 1: memory (per-isolate Map)
↓ tier 2: CF Cache API "platform-cache-apcsrasj-v3"
↓ tier 3: KV snapshot PLATFORM_CACHE_KV
↓ origin:
ApiClient.get("/bff/features") + parallel calls
↓ (delegateCaching: true → no api-client cache)
↓ env.API_SERVICE.fetch(...)

service-api proxy (Frankfurt)
↓ CF Cache API + KV API_CACHE_KV
↓ origin:
BFF /v2/bff/features
↓ mtls (BT) ou cf-worker-key-proxy (CT)
BFF response

service-api writes to its KV/CF

platform-cache writes to memory + CF + KV snapshot

SSR renders → Brand Worker SSR cache writes

CF Zone caches HTML (if anon)

User receives ~200ms total (cold)
~5ms next visit (everything warm)

Invalidação após mudança no backoffice

Caso "mudei a ordem de payment-providers no backoffice":

1. Backoffice → BFF DB write (instant)
2. TTL natural: 5min no service-api (payment-providers rule)
3. Visitor após 5min do TTL: cache miss → re-fetch → fresh data

ou força explícita:

1. DevOps roda manual-cache-purge.yml
tags: payments
scope: all
2. Workflow → POST https://<brand>/api/cache/purge body:{ tags: ["payments"] }
3. Brand worker:
a. engine.purgeByTag("payments") — afeta resources locais com tag "payments"
(atualmente: nenhum no platform-cache; payments mora no service-api)
b. Cascade: env.API_SERVICE.fetch("/__cache_purge__", { originDomain, tags })
4. service-api:
a. Tag purge é brand-wide nesta camada — não há índice tag→key.
Tags são logadas pra observabilidade mas o delete é por originDomain (scope: all entries daquela brand).
A resposta retorna scope: "brand-wide (originDomain; tags/glob ignored)" + intendedPathsIgnored com as paths resolvidas, pra deixar claro que o delete foi mais amplo que o pedido.
(Um índice tag→key real é follow-up futuro; tag-only sem originDomain faz fallback pra purgeAll.)
b. KV delete + CF Cache API best-effort pra todas as entries do originDomain
5. Próxima visita: cache miss → BFF → data fresca

Invalidação em deploy

push main → deploy.yml dispara
→ wrangler deploy --keep-vars --var BUILD_ID:<sha[0:12]>
→ BUILD_ID muda → SSR/API/SPA-shell/assets namespaces ficam órfãos
→ KV entries antigas expiram naturalmente (TTL 12h SSR, 60s shell, 7d assets)
→ POST /api/cache/purge?segments=<csv> — platform-cache purge
→ POST /zones/<id>/purge_cache {purge_everything: true} — CF zone
→ curl -m 8 em 10 paths (warm-up paralelo)

Invariantes arquiteturais

1. Per-user data NEVER entra em cache compartilhado

platform-cache.engine.fetch() rejeita resource names que casem com FORBIDDEN_RESOURCE_PATTERNS (wallet, balance, profile, kyc, auth, rewards, gamification, bets, history, referral, deposit, withdraw, income, bonus, user, etc).

Para per-user, use engine.fetchWithKey(resource, userId, ...) com suffix obrigatório. Ver services/favorites.cache.server.ts como canônico.

Postmortem 2026-04-29: balance leak entre usuários por causa de cache user-scoped sem suffix.

2. api-client.serverCache está em modo delegateCaching no brand worker

app/services/api.server.ts:88 seta delegateCaching: true. O api-client passa direto pro service-api sem consultar GlobalCache nem KV. Isso elimina a duplicação de TTLs entre api-client e platform-cache que causou staleness em /payment-providers e /appearance.

Resources hoje cacheados pelo brand worker passam pelo platform-cache engine ONLY.

3. Tags são vocabulário compartilhado entre brand worker e service-api

platform-cache declara tags: [...] por resource em front-ops/config/cache/defaults.yml. service-api declara tags: [...] por path em front-ops/config/cache/service-api-defaults.yml.

A intersecção (tags como brand, catalog, games:list, payments) permite que um único manual-cache-purge.yml tags: brand ataque ambas as camadas com a mesma semântica.

4. BUILD_ID invalida edge cache, mas NÃO platform-cache

Por design. O platform-cache é stateful entre deploys — repopular o KV snapshot a cada deploy custaria muito ($KV + tempo). Em vez disso:

  • Deploy bump BUILD_ID → SSR/API/SPA-shell viram cold
  • Post-deploy purge explícito é responsável por invalidar platform-cache segments mutáveis (homeRows, brandConfig, etc)

O catch: novo resource adicionado precisa de PR no front-ops pra ser registrado, senão engine.fetch() cai em BYPASS silencioso (com warning console uma vez por isolate).

5. KV snapshot é cross-DC warm tier, NÃO disaster recovery

KvSnapshotStore no platform-cache engine é lido antes da origin quando primary tier (CF Cache API per-DC) é miss. Sem isso, cada datacenter pagaria o custo BFF na 1ª request.

Não confundir com o último-recurso KV snapshot que serve durante origin outage (step 6 do pipeline).

6. CACHE_NAMESPACE separa stage/prod que compartilham ORIGIN_DOMAIN

Cache key format: https://${CACHE_NAMESPACE ?? ORIGIN_DOMAIN}-cache/{keyPrefix}[:suffix]/v1

CACHE_NAMESPACE é injetado pelo front-ops/deploy.yml como worker_name. Garante isolamento entre front-web-stage-vera-bet-br e front-web-vera-bet-br quando o ORIGIN_DOMAIN é compartilhado durante migrations.

7. Hard ceiling de 1h em todos os TTLs de endpoint

Nenhum endpoint cache TTL pode exceder 3600 (1h). Aplicado em:

  • front-ops/config/cache/service-api-defaults.yml (todos os ttl)
  • front-ops/config/cache/defaults.yml (todos os ttlSeconds exceto brandConfig que é 60s)
  • front-service-api CACHE_RULES fallback hardcoded
  • front-web-base API_CACHE_RULES em workers/middleware.ts

O ceiling é estrutural: cada camada aplica Math.min(ttl, 3600) em runtime, então um edit acidental num YAML não pode reintroduzir um TTL maior sem o clamp removê-lo silenciosamente.

staleWhileRevalidateSeconds e staleIfErrorSeconds são janelas de resiliência, não TTLs de endpoint — não estão sujeitos a este ceiling. staleIfErrorSeconds foi reduzido para 6h (21600) em defaults.yml.

8. KV_SSR_TTL — o lever real do "banner levando horas"

O cache de HTML SSR anônimo em KV (workers/middleware.ts, constante KV_SSR_TTL) foi reduzido de 12h para 1h (3600s). Este — não os TTLs de endpoint da plataforma — era o motivo real de banners/mudanças de brand demorarem horas a aparecer após um deploy.

Banner sub-1h sem deploy/purge requer excluir a rota home do SSR-HTML caching ou renderizar o banner client-side a partir do brandConfig (60s). Essa mudança de design fica para spec separado.

9. Generation bump orphana KV (não deleta); TTL-bounded ≤1h

Um bump de CACHE_GENERATION em generation.yml rotaciona o cacheName e o hash de chave em todas as camadas, tornando as entries KV antigas órfãs (não deletadas explicitamente). Essas entries TTL-evict dentro da janela do ceiling (≤1h). Para wipe instantâneo, um POST { all: true } adicional ao service-api seria necessário — isso está documentado como follow-up (D3 Option B), não implementado.

Brand-worker redeploy é obrigatório em um generation bump. O workflow bump-cache-generation.yml agora faz redeploy do brand worker por padrão (brand_workers: all) e FALHA se brand_workers: none for passado, para prevenir que um bump deixe workers em gerações diferentes.

Decisões de design

Por que duas camadas (platform-cache + service-api), não uma?

  • Localidade: brand worker roda no DC mais próximo do user. service-api roda em Frankfurt (perto do BFF). 1 chamada user → brand worker = ~10ms. 1 chamada brand worker → BFF direto = ~300ms (transatlântico). Com service-api: brand worker → Frankfurt (~80ms via Smart Placement) → BFF (~5ms intra-region).
  • Granularidade: platform-cache cacheia payload transformado (homeRows = dezenas de chamadas BFF agregadas + transformadas). service-api cacheia resposta HTTP crua por path. Os dois servem propósitos distintos e coexistem sem duplicação semântica.

Por que api-client não cacheia mais no server?

Resposta longa em Camadas > api-client e na "Invariante 2" acima. Em curto: dois sistemas com TTLs independentes cobrindo as mesmas rotas → drift inevitável → postmortems.

Por que cache tags em vez de só segments?

Tags permitem:

  • Agrupamento lógico sem enumerar nomes (tags: catalog = todos resources de catálogo)
  • Globs pra famílias (games:* cobre games:list, games:detail, games:wins, etc)
  • Vocabulário compartilhado com service-api (mesmo tags: payments purga ambos)

Segments continuam aceitos por backward-compat (deploy.yml legado, automações antigas).

Por que não usar Cache-Tag nativo do Cloudflare (Enterprise feature)?

CT tem Enterprise plan, então tecnicamente disponível. Foi adiado porque:

  1. Requer BFF ou service-api emitir header Cache-Tag em cada response
  2. Funciona só pra cache de zone (não para platform-cache memory tier nem KV snapshot)
  3. App-level tags cobrem 95% do uso e funcionam idênticas em CT e BT

Pode ser uma Fase 6 opcional no futuro pra otimizar SSR HTML caching especificamente.

Por que TTL do brandConfig é tão baixo (60s)?

Trade-off explícito:

  • Antes: TTL 3600s + SWR 300s → 1 hit BFF a cada 1h, mas mudanças no backoffice levavam 1h pra aparecer
  • Agora: TTL 60s + SWR 300s → 1 hit BFF a cada 5min (graças ao SWR), mudanças aparecem em ≤1min

O hit extra no BFF é absorvido pelo single-flight coalescing do platform-cache (única request por isolate por chave). Estimativa: ~20 chamadas BFF/min global (vs 1/h antes) — desprezível pro BFF.

Por que delegateCaching no api-client em vez de remover o cache server-side?

Backward compatibility. Forks e legacy code paths ainda usam api-client direto sem platform-cache acima. A flag permite cutover gradual:

  • Brand worker novo: delegateCaching: true (passa pra platform-cache)
  • Forks/legacy: continuam usando o cache do api-client com TTLs reduzidos (60s)

Eliminação total fica pra um major version bump do api-client.

Configuração end-to-end

Variáveis de ambiente

VariávelOndeValorFunção
BUILD_IDbrand worker12-char SHANamespace de SSR/API caches + tracing header
CACHE_NAMESPACEbrand workerworker_nameIsolation stage/prod
ORIGIN_DOMAINbrand workerhostnameCache key domain
PLATFORM_CACHE_POLICY_JSONbrand workerJSONPolicy do platform-cache
PLATFORM_CACHE_KVbrand workerKV bindingSnapshot tier do platform-cache
CACHE_PURGE_SECRETbrand worker + service-apistringAuth pro /api/cache/purge e /cache_purge
SERVICE_API_CACHE_POLICY_JSONservice-apiJSONPolicy do service-api proxy
API_CACHE_KVservice-apiKV bindingCache cross-DC do proxy

YAML fontes

ArquivoConsumerGeração
front-ops/config/cache/defaults.ymlbrand workerdeploy.yml resolve via yq merge → PLATFORM_CACHE_POLICY_JSON
front-ops/config/cache/service-api-defaults.ymlservice-apifront-service-api/scripts/build-cache-policy.mjs lê → JSON em --var no wrangler deploy
front-ops/config/brands/<brand>/cache.ymlbrand workerOverride per-brand do defaults.yml
front-ops/config/brands/<brand>/environments/<env>/cache.ymlbrand workerOverride per-env

Workflows GH Actions

WorkflowTriggerFunção
deploy.ymlpush/dispatchDeploy brand worker + post-deploy purge + warmup
manual-cache-purge.ymldispatchPurge ondemand com tags/glob/segments + scope (platform-cache, service-api, zone, all)
cache-status.ymldispatchInspeciona estado live (TTLs restantes) via /api/cache/inspect

Arquivos relevantes

Core (@cactus-agents/*)

  • packages/platform-cache/src/engine.ts — pipeline + purgeByTag/Glob
  • packages/platform-cache/src/policy.ts — parseCachePolicy, findResourcesByTag, listAllTags
  • packages/platform-cache/src/purge-handler.ts — createPurgeHandler aceita tags/glob/segments
  • packages/platform-cache/src/types.ts — ResourcePolicy.tags, CachePolicy
  • packages/platform-cache/src/stores/ — CacheApiStore, KvSnapshotStore, NoopSnapshotStore
  • packages/api-client/src/cache.ts — ServerCacheConfig.delegateCaching, DEFAULT_API_CACHE_RULES (60s)
  • packages/api-client/src/client.ts — getCacheInfo respeita delegateCaching

Base (front-web-base)

  • workers/middleware.ts — SSR/API/asset/SPA-shell caches
  • app/services/platform-cache.server.ts — singleton sharedPrimaryStore + factory
  • app/services/api.server.ts — createCactusServerClient com delegateCaching: true
  • app/services/brand.server.ts — engine.fetch("brandConfig")
  • app/services/games.cache.server.ts — engine.fetch p/ todos resources de games
  • app/routes/api/cache/purge.ts — endpoint /api/cache/purge (tags/glob/segments + cascade)
  • app/routes/api/cache/inspect.ts — endpoint /api/cache/inspect (live state)
  • app/routes/api/dev/cache-policy.ts — endpoint /api/dev/cache-policy (declared)

service-api (front-service-api)

  • src/index.ts — proxy logic, loadCacheRules, handleCachePurge (com tags forward-compat)
  • scripts/build-cache-policy.mjs — lê YAML do front-ops, emite JSON
  • package.json scripts: deploy:ct, deploy:bt (injeta SERVICE_API_CACHE_POLICY_JSON)

front-ops (front-ops)

  • config/cache/defaults.yml — platform-cache policy SoT
  • config/cache/service-api-defaults.yml — service-api policy SoT
  • .github/workflows/deploy.yml — deploy brand worker + post-deploy purge + warmup
  • .github/workflows/manual-cache-purge.yml — purge ondemand
  • .github/workflows/cache-status.yml — inspect ondemand