Blog Arquitectura Neges

Cache e invalidacion en Neges Storefront: del precio pegado al edge sano

Post-mortem tecnico de la estrategia de cache del storefront: edge cache en Vercel, invalidacion por tags desde Strapi, precios pegados, self-calls SSR y el interceptor in-process que cerro la causa raiz.

25 min de lectura

El storefront publico de Neges necesita ser rapido, barato y fresco al mismo tiempo. La solucion combina cache en el edge de Vercel, invalidacion por tags desde Strapi y un interceptor SSR que evita que la app se pida datos a si misma pasando por el edge.

Resumen ejecutivo

Neges Storefront es multi-tenant, usa Angular SSR en Vercel y toma sus datos desde Strapi en Railway. Para que las tiendas respondan rapido se introdujo cache en el edge de Vercel con invalidacion por tags ante ediciones de contenido.

Durante la puesta en marcha aparecio un sintoma peligroso: un operador cambiaba un precio en el backoffice y la tienda seguia mostrando el valor anterior, incluso despues de esperar y refrescar. La API de Strapi devolvia el valor correcto, pero el HTML cacheado mantenia un precio viejo horneado en la hidratacion.

El problema no era una sola cosa. Habia tres capas apiladas: invalidacion inerte por variables desalineadas, cache en memoria del SSR con TTL demasiado largo y, finalmente, un loop de envenenamiento donde el SSR hacia self-calls hacia su host publico y recibia datos stale desde el edge.

El resultado final: una edicion de precio se propaga sola en segundos. La primera visita tras una edicion puede ver el valor viejo una unica vez por stale-while-revalidate, y desde la revalidacion siguiente el edge queda con el valor nuevo.

El incidente: precios pegados

El sintoma reportado fue directo: cambio un precio en backoffice y en la tienda no se actualiza. En un caso concreto, la ficha de una churrera manual, codigo EVP3-H, mostraba $162.082 en el home y en el detalle, cuando el precio correcto de lista base era $162.080.

  • Strapi, la fuente de verdad, devolvia siempre el valor correcto: $162.080.
  • Un render fresco con parametro anti-cache tambien salia correcto.
  • Las copias servidas desde el edge, creadas pocos minutos antes y con el bundle JS actual, tenian el precio viejo horneado en el HTML.
  • Refrescar podia incluso regenerar el problema porque la revalidacion volvia a armar HTML fresco con datos stale.

Modelo mental: actores y capas

Para entender la arquitectura y el bug hay que separar tres capas: el edge que sirve copias, la funcion SSR que fabrica HTML y Strapi como fuente de verdad.

capas-cache-storefront.mmdmermaid
El diagrama se renderiza al cargar la pagina.

El edge existe porque armar una pagina puede tardar entre 1 y 5 segundos, mientras entregar una copia puede rondar decenas de milisegundos. Esa copia tiene TTL y tags. Cuando cambia contenido, Strapi marca como stale las copias con tenant:<slug>.

Tres causas apiladas

CausaSintomaResolucion
Invalidacion inerteStrapi no estaba purgando el edge porque habia nombres de variables de entorno desalineados.Se reutilizo la configuracion de Vercel que Strapi ya tenia para automatizacion de dominios.
Cache in-memory del SSR con TTL 60 sUna revalidacion del edge podia tomar datos viejos desde la cache interna de la funcion.El TTL default bajo a 5 s, suficiente para deduplicar el doble-resolve e insuficiente para esconder ediciones.
Loop de envenenamiento por SSR self-callEl SSR fabricaba HTML nuevo, pero pedia datos a su propio host publico y el edge le devolvia JSON stale.Se agrego un interceptor server-only que resuelve catalogo y producto in-process, directo a Strapi.

Causa raiz: SSR self-call por el edge

La fabrica SSR necesitaba datos del producto o catalogo. En vez de pedirlos directo a Strapi, construia una URL con el host publico y hacia fetch a /storefront-api. Esa llamada salia de la funcion, entraba de nuevo por Vercel Edge y quedaba sometida al mismo stale-while-revalidate que las visitas reales.

loop-envenenamiento-edge.mmdmermaid
El diagrama se renderiza al cargar la pagina.

Por eso esperar y refrescar no bastaba. Cada refresh podia disparar una revalidacion que volvia a contaminar el HTML. No era un vencimiento que se curaba con tiempo; era una fuente que se re-contaminaba a si misma.

La solucion en cinco piezas

La arquitectura final mezcla velocidad, frescura y aislamiento multi-tenant. Las tres primeras piezas hacen que la tienda sea rapida; las dos ultimas cierran los agujeros de staleness.

PiezaRepoRol
Edge cache de HTML y APIs publicasneges-storeCachea respuestas anonimas con Vercel-CDN-Cache-Control y Vercel-Cache-Tag.
Resolucion de precios en loteneges-strapiElimina N+1 del catalogo y mantiene paridad con la resolucion individual.
Invalidacion por tags desde Strapineges-strapiDetecta mutaciones por document-service y purga tenant:<slug> en Vercel.
TTL corto de cache SSR in-memoryneges-storeDeduplica el doble-resolve sin esconder ediciones.
Interceptor SSR in-processneges-storeEvita self-calls al host publico durante SSR y lee directo desde Strapi.

Pieza 1: edge cache de HTML y APIs publicas

En neges-store/src/server-app.ts, las respuestas publicas anonimas setean headers separados para navegador y edge. El navegador siempre revalida; el edge puede guardar copias por TTL y asociarlas a tags para purga selectiva.

headers-cache-storefront.txttext
Cache-Control:            public, max-age=0, must-revalidate
Vercel-CDN-Cache-Control: public, max-age=<ttl>, stale-while-revalidate=<swr>
Vercel-Cache-Tag:         tenant:<slug>, tenant-domain:<host>
Vercel-CDN-Cache-Control afecta al edge; el comprador no sirve precios viejos desde su cache local.
SuperficieTTL edgeSWR
Home, categoria, colecciones, busqueda y legales HTML300 s3600 s
Ficha de producto HTML y /storefront-api/products/:slug900 s86400 s
/storefront-api/catalog300 s3600 s
  • Siempre no-store: checkout, ordenes, pagos, customer-access, preview de Storefront Studio y respuestas no-200.
  • El edge indexa por host, path y query, asi que los tenants no comparten entradas.
  • tenant:<slug> es el tag primario de purga; tenant-domain:<host> queda como respaldo por dominio.

Pieza 2: precios en lote

El catalogo resolvia precios producto por producto, con alrededor de 352 consultas por render. Se agrego resolveProductPricesForList en neges-strapi/src/api/product-price/services/pricing.ts para resolver la lista completa con una consulta product_prices filtrada por product.documentId.

  • Reusa toProductPriceSnapshot.
  • Mantiene filtros de vigencia y canal.
  • Mantiene compareResolvedPrices.
  • Mantiene calculatePriceBreakdown.
  • Incluye test de paridad contra la resolucion individual.
  • El TTFB del catalogo bajo de aproximadamente 3.4-4.5 s a aproximadamente 0.7 s.

Pieza 3: invalidacion por tags desde Strapi

neges-strapi/src/utils/vercel-cache-invalidation.ts registra un middleware del document-service. Ante mutaciones de product, product-price, product-category, catalog-collection, storefront-studio-config, tenant-domain o tenant, resuelve el slug del tenant y llama a la REST API de Vercel invalidate-by-tags.

vercel-invalidate-by-tags.txttext
POST https://api.vercel.com/v1/edge-cache/invalidate-by-tags
?projectIdOrName=<VERCEL_STOREFRONT_PROJECT_ID>&teamId=<VERCEL_TEAM_ID>
Authorization: Bearer <VERCEL_TOKEN>
Content-Type: application/json

{
  "tags": ["tenant:importadora-roble"],
  "target": "production"
}
  • Se usa invalidate, no delete: marca stale y deja que SWR revalide en background.
  • Un solo tag tenant:<slug> cubre dominio custom y subdominio .neges.cl.
  • Como engancha document-service, purga ediciones desde admin panel, REST y neges-mcp.
  • No hace falta una tool explicita de revalidar para ChatGPT.
  • Reusa VERCEL_TOKEN, VERCEL_STOREFRONT_PROJECT_ID y VERCEL_TEAM_ID desde config/domainAutomation.ts.
  • Si falta token o project id, el feature queda como no-op. Si Vercel responde 403, se loguea como warn y no rompe la edicion.

Pieza 4: TTL corto de cache SSR in-memory

El SSR mantiene una cache en memoria por lambda en storefront.server-api.ts, controlada por CATALOG_CACHE_TTL_MS y PRODUCT_CACHE_TTL_MS. Su razon de ser no es frescura de negocio, sino deduplicar el doble-resolve de un mismo render.

Ese doble-resolve ocurre en menos de un segundo: una resolucion para detectar redirect o 404 y otra durante el render Angular. Con TTL 60 s, el edge podia revalidar y tomar datos viejos desde la memoria de la funcion. Con 5 s, se mantiene el dedup sin enmascarar ediciones.

Pieza 5: interceptor SSR in-process

El fix de causa raiz vive en neges-store/src/app/core/storefront/storefront-ssr-data.interceptor.ts y se registra solo en app.config.server.ts. En servidor, los GET a /storefront-api/catalog y /storefront-api/products/:slug se resuelven llamando directamente a getStorefrontCatalogData y getStorefrontProductData.

ssr-interceptor-fresco.mmdmermaid
El diagrama se renderiza al cargar la pagina.
  • Ya no hay fetch que salga y vuelva a entrar por el edge durante SSR.
  • Se usa el mismo codigo de lectura que server-app.ts.
  • Se elimina un viaje edge + funcion por pagina SSR.
  • Se reduce aproximadamente a la mitad la cantidad de invocaciones relacionadas con ese self-call.
  • La cache JSON del edge queda para navegaciones del navegador, donde un unico stale post-purga es aceptable.

Flujo correcto de una edicion de precio

edicion-precio-cache.mmdmermaid
El diagrama se renderiza al cargar la pagina.

Referencia de componentes y archivos

PiezaRepo / archivoQue hace
Headers de edge cache + tagsneges-store · src/server-app.ts (setPublicEdgeCacheHeaders, isPrivateStorefrontPath)Setea Vercel-CDN-Cache-Control y Vercel-Cache-Tag en HTML catch-all y endpoints JSON.
Interceptor SSR in-processneges-store · src/app/core/storefront/storefront-ssr-data.interceptor.ts y src/app/app.config.server.tsResuelve catalog/product en server sin pasar por el edge.
Cache in-memory SSRneges-store · src/app/core/storefront/storefront.server-api.tsDedup del doble-resolve con TTL 5 s.
URL de datos del SSRneges-store · src/app/core/storefront/storefront-api.service.tsbuildApiUrl y getOrigin explican por que existia el self-call.
Precios en loteneges-strapi · src/api/product-price/services/pricing.tsresolveProductPricesForList, una consulta por lista y paridad con resolucion individual.
Payload storefrontneges-strapi · src/api/product/services/storefront-product.tsArma catalogo y detalle usando el batch de precios.
Invalidacion por tagsneges-strapi · src/utils/vercel-cache-invalidation.ts y src/index.tsMiddleware document-service hacia REST invalidate-by-tags de Vercel.
Config Vercel reusadaneges-strapi · config/domainAutomation.tsToken, project id y team id compartidos con automatizacion de dominios.

Configuracion

Variable Strapi en RailwayUso
VERCEL_TOKENToken de Vercel con permiso de cache purging, compartido con automatizacion de dominios.
VERCEL_STOREFRONT_PROJECT_IDID del proyecto neges-store en Vercel.
VERCEL_TEAM_IDID del team de Vercel.
Variable Store en VercelDefaultUso
STOREFRONT_CATALOG_CACHE_TTL_MS5000TTL de la cache in-memory del catalogo durante SSR.
STOREFRONT_PRODUCT_CACHE_TTL_MS= catalogTTL de la cache in-memory de la ficha de producto durante SSR.

La taxonomia actual emite y purga a nivel tenant: tenant:<slug> como tag primario y tenant-domain:<host> como respaldo. En Vercel, el documento base considera limites de 256 caracteres por tag, 16 tags por llamada REST y coma como separador reservado.

Decisiones y tradeoffs

DecisionPor queAlternativa descartada
Interceptor in-process en vez de arreglar la URL del self-callElimina el round-trip, usa el mismo codigo que server-app.ts y mejora dedup/invocaciones.Apuntar el self-call a un origin interno seguia dejando una capa de red fragil y riesgo de re-cache.
invalidate en vez de deleteSirve stale al instante y revalida en background, sin latencia ni stampede.Delete da frescura en el primer request, pero puede golpear el origen en masa. Queda como posible modo urgente.
Reusar VERCEL_TOKEN existenteCero variables nuevas y menos friccion operativa.Token dedicado de purga: mas higienico, pero mas operacion.
Tag grueso por tenantUn tag cubre todos los dominios del tenant y es simple.Tags finos por producto/categoria: menos re-render, mas complejidad y mapping por documentId.
Bruto derivado del neto en StrapiEl precio con IVA se calcula y redondea una vez en backend.Recalcular en frontend invita a divergencias de redondeo.
No crear modulo runtime de TTLsEnv vars alcanzan y evitan una consulta o cache extra por pagina.Config por tenant tenia mas costo que beneficio.
No crear boton forzar refreshCon interceptor y purga automatica el sistema se autocura en segundos.Endpoint admin con delete sigue siendo viable para emergencias futuras.

Runbook operativo

Cuando alguien reporta que una pagina no se actualiza, el diagnostico debe separar fuente de verdad, render fresco y copia del edge. Asi se evita culpar al backend cuando el problema es staleness, o culpar al cache cuando el dato base sigue mal.

diagnostico-cache.shbash
# 1) Ver estado de copia del edge y antiguedad
curl -sI "https://importadoraroble.cl/products/<slug>" | grep -i "x-vercel-cache\|^age:"

# 2) Comparar copia normal vs render fresco con parametro anti-cache
curl -s --compressed "https://importadoraroble.cl/products/<slug>" -o /tmp/cacheado.html
curl -s --compressed "https://importadoraroble.cl/products/<slug>?cb=$(date +%s)" -o /tmp/fresco.html

# 3) Fuente de verdad: Strapi sin cache del edge
curl -s --compressed -H "X-Store-Host: importadoraroble.cl" \
  "https://api.neges.cl/api/storefront/products/<slug>"
  • Si Strapi todavia devuelve el valor viejo, el problema es de datos o edicion, no de cache.
  • Si el render fresco es correcto y la copia normal no, es cache del edge: esperar el proximo request o forzar purga.
  • Si el render fresco tambien esta mal, revisar codigo y ensamblado del payload en Strapi.
  • Revisar logs de Strapi por [cache-invalidation] purged Vercel edge cache tags.
  • Revisar logs por Vercel purge failed 403 si el token no tuviera permiso de purga.
  • Para forzar purga sin editar, llamar el endpoint invalidate-by-tags con tenant:<slug>.

Como evolucionar sin reabrir el bug

  • Agregar tags finos por producto/categoria: tenant:<slug>:product:<documentId>, :category:<id> y :catalog. Usar documentId, no slug, porque el slug cambia.
  • Mantener home y catalogo invalidandose cuando una edicion afecta listados.
  • Agregar modo urgente con dangerously-delete-by-tag solo para bajas criticas que no toleran ni un stale.
  • No reintroducir self-calls por el edge. Toda lectura que el SSR necesite debe ser in-process o directa a Strapi.
  • No recalcular bruto con IVA en el frontend. Usar pricing.unitGrossAmount del payload.
  • Mantener cache in-memory SSR con TTL menor o igual a 5 s.
  • Recordar el contrato con neges-mcp: sus ediciones pasan por document-service y purgan igual; si cambia la forma consumida por MCP, actualizar neges-mcp segun neges-strapi/MCP-CONTRACT.md.

Changelog relevante

RepoCommitContenido
neges-store75558dfTune static asset cache-control headers, immutable en JS/CSS hasheados.
neges-storec976dbfAdd Vercel edge cache + featured badge to product card.
neges-store6bdadf0Doc: clarify cache invalidation reuses existing Vercel config.
neges-store568737dReduce SSR catalog memory cache TTL to 5s.
neges-store5ba2ab3Bypass edge self-calls in SSR storefront: interceptor y fix de causa raiz.
neges-strapib2ba4deOptimize storefront product queries con shape liviano de catalogo.
neges-strapib149b98Add batch pricing and Vercel cache invalidation.
neges-strapi02cc433Use domain config for Vercel cache invalidation.
neges-strapif83e92aClean up branding media and log cache purges.

Glosario

TerminoDefinicion
SSRServer-Side Rendering: el HTML se arma en Vercel Function antes de llegar al navegador.
Edge / CDNRed de servidores cercanos al usuario que guardan copias. Para Chile, el PoP relevante del documento es Sao Paulo, gru1.
TTLTime To Live: cuanto vive una copia antes de considerarse vencida.
SWRStale-while-revalidate: servir copia vieja al instante mientras se arma una nueva en background.
Cache tagEtiqueta de una copia cacheada para invalidarla selectivamente, por ejemplo tenant:importadora-roble.
Invalidate vs DeleteInvalidate marca stale y revalida despues; Delete borra y obliga al proximo request a esperar origen.
Self-callCuando el servidor hace una peticion HTTP a su propio host publico en vez de resolver en memoria.
x-vercel-cacheHeader de Vercel que puede mostrar HIT, MISS o STALE segun el estado de la copia.

Recursos relacionados

¿Te fue útil este contenido?

Tu feedback nos ayuda a mejorar la documentación de Neges.

Soporte humano

¿No encuentras lo que buscas?

Conversa con nuestro equipo de soporte en español. Respondemos por WhatsApp, correo o ticket en menos de 24 horas hábiles.

contacto@neges.clLun a Vie · 9 a 18 hrs
+56 9 0000 0000WhatsApp Business