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-cachesegments 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 osttl)front-ops/config/cache/defaults.yml(todos osttlSecondsexcetobrandConfigque é 60s)front-service-apiCACHE_RULESfallback hardcodedfront-web-baseAPI_CACHE_RULESemworkers/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:*cobregames:list,games:detail,games:wins, etc) - Vocabulário compartilhado com service-api (mesmo
tags: paymentspurga 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:
- Requer BFF ou service-api emitir header
Cache-Tagem cada response - Funciona só pra cache de zone (não para platform-cache memory tier nem KV snapshot)
- 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ável | Onde | Valor | Função |
|---|---|---|---|
BUILD_ID | brand worker | 12-char SHA | Namespace de SSR/API caches + tracing header |
CACHE_NAMESPACE | brand worker | worker_name | Isolation stage/prod |
ORIGIN_DOMAIN | brand worker | hostname | Cache key domain |
PLATFORM_CACHE_POLICY_JSON | brand worker | JSON | Policy do platform-cache |
PLATFORM_CACHE_KV | brand worker | KV binding | Snapshot tier do platform-cache |
CACHE_PURGE_SECRET | brand worker + service-api | string | Auth pro /api/cache/purge e /cache_purge |
SERVICE_API_CACHE_POLICY_JSON | service-api | JSON | Policy do service-api proxy |
API_CACHE_KV | service-api | KV binding | Cache cross-DC do proxy |
YAML fontes
| Arquivo | Consumer | Geração |
|---|---|---|
front-ops/config/cache/defaults.yml | brand worker | deploy.yml resolve via yq merge → PLATFORM_CACHE_POLICY_JSON |
front-ops/config/cache/service-api-defaults.yml | service-api | front-service-api/scripts/build-cache-policy.mjs lê → JSON em --var no wrangler deploy |
front-ops/config/brands/<brand>/cache.yml | brand worker | Override per-brand do defaults.yml |
front-ops/config/brands/<brand>/environments/<env>/cache.yml | brand worker | Override per-env |
Workflows GH Actions
| Workflow | Trigger | Função |
|---|---|---|
deploy.yml | push/dispatch | Deploy brand worker + post-deploy purge + warmup |
manual-cache-purge.yml | dispatch | Purge ondemand com tags/glob/segments + scope (platform-cache, service-api, zone, all) |
cache-status.yml | dispatch | Inspeciona estado live (TTLs restantes) via /api/cache/inspect |
Arquivos relevantes
Core (@cactus-agents/*)
packages/platform-cache/src/engine.ts— pipeline + purgeByTag/Globpackages/platform-cache/src/policy.ts— parseCachePolicy, findResourcesByTag, listAllTagspackages/platform-cache/src/purge-handler.ts— createPurgeHandler aceita tags/glob/segmentspackages/platform-cache/src/types.ts— ResourcePolicy.tags, CachePolicypackages/platform-cache/src/stores/— CacheApiStore, KvSnapshotStore, NoopSnapshotStorepackages/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 cachesapp/services/platform-cache.server.ts— singleton sharedPrimaryStore + factoryapp/services/api.server.ts— createCactusServerClient com delegateCaching: trueapp/services/brand.server.ts— engine.fetch("brandConfig")app/services/games.cache.server.ts— engine.fetch p/ todos resources de gamesapp/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 JSONpackage.jsonscripts:deploy:ct,deploy:bt(injeta SERVICE_API_CACHE_POLICY_JSON)
front-ops (front-ops)
config/cache/defaults.yml— platform-cache policy SoTconfig/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