Saltar a contenido

Runbook

Variables de entorno

El único chequeo automático es check-env-variables.js, que corre al inicio de next.config.js y aborta el build si falta NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY. Todas las demás variables faltan en silencio y fallan en runtime.

Variable Lado Sin ella
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ambos El build aborta con error explícito, y el contenedor no arranca
MEDUSA_BACKEND_URL server, runtime El middleware lanza Error fetching regions: no carga ninguna página
NEXT_PUBLIC_MEDUSA_BASE_URL navegador, build El SDK y todo el código de cliente caen a http://localhost:9000
NEXT_PUBLIC_DEFAULT_REGION ambos Se asume us
NEXT_PUBLIC_BASE_URL ambos URLs absolutas mal armadas
PAYLOAD_PUBLIC_URL, PAYLOAD_USER_EMAIL, PAYLOAD_USER_PASSWORD server Home sin contenido, menú vacío, /api/credit-form en 500. Sin las credenciales, además, la vista previa muestra la versión publicada en lugar del borrador sin dar ningún error
NEXT_PUBLIC_ALGOLIA_APP_ID, NEXT_PUBLIC_ALGOLIA_SEARCH_KEY, NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX, NEXT_PUBLIC_ALGOLIA_SUGGESTIONS_INDEX browser Búsqueda y grillas de la home sin resultados
ALGOLIA_ADMIN_API_KEY server Fallan los scripts de indexación (scripts/populate-suggestions.js)
NEXT_PUBLIC_STRIPE_KEY browser No se puede pagar con tarjeta
NEXT_PUBLIC_GOOGLE_CLIENT_ID browser Sin login con Google
NEXT_PUBLIC_RECAPTCHA_SITE_KEY navegador, build Sin ella el proveedor de reCAPTCHA no se monta y el botón de crédito falla con "GoogleReCaptcha Context has not yet been implemented". Se inlinea en el build: cargarla en el secret no alcanza si no llega como build-arg. El dominio del ambiente debe estar autorizado en la consola de Google
RECAPTCHA_SECRET_KEY server, runtime La valida el backend. Ojo con el par: si el backend la tiene y el front no manda token, /store/credit-request responde 400; si el backend no la tiene, omite la verificación
NEXT_PUBLIC_BANCARD_QR_SIMULATOR, NEXT_PUBLIC_BANCARD_VPOS_SIMULATOR browser (solo desarrollo) Sin ellas no aparecen los botones del simulador de Bancard; deben ir junto con BANCARD_QR_SIMULATOR / BANCARD_VPOS_SIMULATOR en el backend
GEOZONES_REVALIDATE_SECONDS server La lista de ciudades del checkout se guarda 300 s por defecto; 0 desactiva el caché
NEXT_PUBLIC_BANCARD_VPOS_ORIGIN browser Sin pago con tarjeta: el origen de Bancard falta en la CSP y no se elige el script (:8888 en staging)
NEXT_DEV_ORIGINS server (solo desarrollo) Hosts además de localhost por los que se entra al dev server (ej. 192.168.0.220). Sin ella, Next 16 bloquea el HMR y rechaza las acciones de servidor al entrar por IP o dominio: la tienda carga pero ningún botón hace nada
REVALIDATE_SECRET server Revalidación bajo demanda sin proteger
NEXT_PUBLIC_PAYLOAD_URL navegador, build Vista previa del CMS a medias: no se refresca sola al guardar, y frame-ancestors queda en 'none', así que con CSP_ENFORCE=true el panel queda en blanco. Se inlinea en el build: ponerla solo en el .env del host no tiene efecto
PREVIEW_SECRET server, runtime /api/draft responde 501 y no hay vista previa de borradores. Tiene que ser idéntica al PREVIEW_SECRET del CMS
CSP_ENFORCE server, runtime Sin ella la política de seguridad de contenido va en modo reporte: el navegador avisa en consola pero no bloquea. Con true bloquea de verdad. Ver más abajo
COTIZACION_PUNTOS_AJUSTE server, runtime Se asume 50, el mismo ajuste que el integrador, sobre la tasa de venta de Cambios Chaco

Un valor con # va entre comillas

dotenv —lo que usan Next y Payload para leer los .env— interpreta # como inicio de comentario y trunca el valor. source de bash no lo hace, así que la prueba manual pasa y la aplicación falla. Pasó con PAYLOAD_USER_PASSWORD: 17 caracteres por bash, 15 por dotenv, y el CMS respondía 401. Escribir CLAVE="valor#con#almohadilla".

Las NEXT_PUBLIC_* son públicas

Next.js las inlinea en el bundle del navegador. ALGOLIA_ADMIN_API_KEY, RECAPTCHA_SECRET_KEY, PAYLOAD_USER_PASSWORD y REVALIDATE_SECRET nunca deben renombrarse con ese prefijo.

Deploy

No hay pipeline de deploy en este repositorio

El único workflow de GitHub Actions es .github/workflows/notificar-portal.yml (avisa al portal de documentación). .github/ISSUE_TEMPLATE/ viene heredado del starter de Medusa. El deploy se hace a mano, con Docker. Quién lo ejecuta y sobre qué host no está registrado en el repo — ver Pendiente al final.

El artefacto es la imagen definida en Dockerfile: build multi-stage sobre node:24-alpine que instala con npm ci, corre npm run build con output: "standalone" y en la segunda etapa copia el servidor autocontenido (.next/standalone) más .next/static, public/ y check-env-variables.js. Expone el puerto 8000 y arranca con node server.js, previa validación de entorno. Pesa unos 235 MB — ver ADR-0005.

# En el host, sobre la rama a desplegar
git pull
docker compose build --no-cache     # NEXT_PUBLIC_MEDUSA_BACKEND_URL se pasa como ARG
docker compose up -d

docker-compose.yml levanta el servicio storefront como contenedor medusa_storefront, publica 8000:8000, usa restart: unless-stopped y lee env_file: .env.local — la misma fuente que consume el build.

PORT se fija en el compose a propósito

.env.local define PORT=8001 para el dev local. El server.js del standalone lee PORT y se ataría a 8001 dentro del contenedor, dejándolo inalcanzable detrás del mapeo 8000:8000. El bloque environment del compose lo fija en 8000 y gana sobre env_file.

Las NEXT_PUBLIC_* se congelan en el build

Se inlinean durante npm run build, dentro de la imagen. Cambiar una en el .env del host y reiniciar el contenedor no tiene efecto: hay que reconstruir la imagen.

Política de seguridad de contenido (CSP)

La política se arma en src/proxy.ts con un nonce por petición y sale en una de dos cabeceras, según CSP_ENFORCE:

CSP_ENFORCE Cabecera Qué hace el navegador
sin definir o cualquier otro valor Content-Security-Policy-Report-Only Avisa en consola lo que bloquearía. Modo por defecto
true Content-Security-Policy Bloquea lo que no está en la política

Es una variable de runtime: se cambia en el .env del ambiente y se reinicia el contenedor. No hace falta reconstruir la imagen ni desplegar, y volver atrás es el mismo gesto al revés. Por eso conviene activarla en staging antes que en producción.

# Comprobar en qué modo está un ambiente
curl -sD - -o /dev/null https://stg-store.compulandia.com.py/py | grep -i content-security-policy

Cómo pasar un ambiente a bloqueante

  1. Con el ambiente en modo reporte, recorrer con la consola del navegador abierta: home, búsqueda, ficha de producto, carrito, checkout completo (incluido el pago con tarjeta y el QR) y la página del pedido.
  2. Anotar cada aviso de la política. Cada uno es un origen que falta o un script sin nonce.
  3. Corregir: si es un origen legítimo, agregarlo a la directiva que corresponda en proxy.ts; si es un script inyectado sin nonce, resolverlo donde se inyecta.
  4. Con la consola limpia, poner CSP_ENFORCE=true y reiniciar.
  5. Recorrer lo mismo otra vez. Ante cualquier rotura, sacar la variable y reiniciar: vuelve al modo reporte al instante.

Cloudflare Zaraz

Zaraz inyecta su script después de que la respuesta sale de Next, así que esa etiqueta no lleva nuestro nonce. Como la política usa 'strict-dynamic', agregar el dominio no alcanza: un script sin nonce se bloquea igual. Hay que verificarlo en staging antes de activar el bloqueo en producción, y resolverlo del lado de Cloudflare.

Señal de salud

GET /api/health responde por la aplicación, no por sus dependencias: no consulta Medusa, Payload ni Algolia, y el matcher del middleware excluye /api, así que tampoco dispara el fetch de regiones que tumba el resto del sitio cuando Medusa no responde.

{ "status": "ok", "commit": "abe849f", "version": "v1.3.0",
  "uptime_s": 42, "timestamp": "2026-08-27T13:28:15.437Z" }
Campo Para qué
commit y version Confirman qué imagen está corriendo. Los inyecta el Dockerfile como ENV
uptime_s Un valor bajo confirma que el contenedor se recreó y no que quedó el viejo arriba

Son dos señales distintas y conviene no confundirlas: /api/health en 200 con /py en 500 significa que el front está sano y que el backend no responde.

En healthchecks usar 127.0.0.1, nunca localhost

Dentro del contenedor localhost resuelve a ::1 (IPv6) y el servidor escucha en 0.0.0.0 (IPv4). busybox wget no reintenta con la otra familia: con localhost el healthcheck falla siempre, con la aplicación funcionando perfecto.

Logs

  • Aplicación: docker compose logs -f storefront (o docker logs -f medusa_storefront). Es stdout de Next.js; no hay archivo de log ni agregador configurado.
  • next.config.js tiene logging.fetches.fullUrl = true: el log del servidor muestra la URL completa de cada fetch a Medusa y a Payload. Es la forma más rápida de ver si el backend responde y con qué URL se lo está llamando.
  • Los proxies a Payload loguean con prefijo (🛠️ Next.js API Header, 📡 API Route llamando a Payload:) en src/api/header/route.ts.
  • No hay Sentry ni monitoreo de errores. Un error de runtime en producción solo se ve en el stdout del contenedor.

Rollback

No hay registro de imágenes ni tags: la imagen se construye en el host desde el código. El rollback es volver al commit anterior y reconstruir.

git log --oneline -10          # identificar el commit bueno
git checkout <commit-anterior>
docker compose build --no-cache && docker compose up -d
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/py   # espera 200

Tarda lo que tarde el build completo (npm ci + next build), del orden de minutos. Mejora pendiente: taguear imágenes por commit para poder volver sin recompilar.

Fallas frecuentes

1. Cae todo el sitio: Error fetching regions — nombre de la variable del backend

Síntoma. Ninguna página carga; el log muestra el error del middleware pidiendo MEDUSA_BACKEND_URL.

Evidencia. Cinco commits consecutivos con el mismo mensaje —39f32b3, 15b16d1, 786d3de, 4f50f40, 0ef751a, "cambio de nombre de variable de entorno del backend"— más un sexto (21111cc, "cambio de puerto de produccion"). Llegaron a convivir tres nombres para la misma URL:

Nombre Dónde se leía
MEDUSA_BACKEND_URL src/middleware.ts — el único que el middleware acepta
NEXT_PUBLIC_MEDUSA_BASE_URL src/lib/config.ts (primera opción del SDK)
NEXT_PUBLIC_MEDUSA_BACKEND_URL Botón de login con Google. Eliminada el 2026-08-27

El propio mensaje de error del middleware advierte que la variable "ya no se llama NEXT_PUBLIC_MEDUSA_BACKEND_URL".

Solución. Quedan dos nombres, no tres: NEXT_PUBLIC_MEDUSA_BACKEND_URL se eliminó el 2026-08-27 (tenía un solo consumidor, el botón de login con Google). Definir los dos restantes apuntando a la misma URL.

Existen por razones distintas y no son intercambiables:

Variable Dónde se lee Para cambiarla
MEDUSA_BACKEND_URL src/middleware.ts, en runtime Reiniciar el contenedor
NEXT_PUBLIC_MEDUSA_BASE_URL SDK y código de cliente, inlineada en el build Reconstruir la imagen

MEDUSA_BACKEND_URL no sirve como fallback de la pública

src/lib/config.ts y use-bancard-payment-sse.ts la usaban así. En el navegador vale undefined —solo se inlinean las NEXT_PUBLIC_*—, de modo que el fallback nunca cubría el caso que aparentaba cubrir: si faltaba la pública, el cliente caía directo a localhost:9000. Se eliminaron los dos.

2. La búsqueda no devuelve nada: la key de Algolia que no existe

Síntoma. El buscador no responde o falla al inicializar el cliente.

Evidencia. src/lib/search-client.ts lee process.env.NEXT_PUBLIC_ALGOLIA_API_KEY, variable que no está definida en el entorno (el .env.local define NEXT_PUBLIC_ALGOLIA_SEARCH_KEY). Está registrado como problema CRÍTICO n.º 1 en el plan de resolución de problemas, y verificado como vigente en develop al 2026-08-19.

Solución. Corregir el código a NEXT_PUBLIC_ALGOLIA_SEARCH_KEY (arreglo de fondo) o, como parche de entorno, definir también NEXT_PUBLIC_ALGOLIA_API_KEY con el mismo valor y reconstruir la imagen.

3. La home devuelve 404 en regiones nuevas

Síntoma. Se da de alta una región en Medusa Admin, el catálogo funciona en /{cc}/store, pero /{cc} responde 404.

Evidencia. src/app/[countryCode]/(main)/page.tsx tiene la lista fija const ALLOWED = ["py", "ar", "br"] y llama a notFound() para cualquier otro código. Registrado como problema n.º 3 del mismo plan y verificado como vigente al 2026-08-19.

Solución. Agregar el código de país a ALLOWED, o reemplazar la lista fija por una validación contra las regiones de Medusa.

4. Home vacía: Payload no responde

Síntoma. La página muestra "No se encontró la homepage en Payload" o el menú de categorías aparece vacío, con el resto del sitio funcionando.

Evidencia. El fallback está escrito en la propia home; src/lib/payload.ts avisa por consola cuando faltan PAYLOAD_PUBLIC_URL, PAYLOAD_USER_EMAIL o PAYLOAD_USER_PASSWORD, y devuelve null en vez de romper.

Solución. Verificar que Payload esté arriba y que exista el slug homepage publicado. El token JWT se cachea en memoria del proceso: si se rotaron las credenciales, reiniciar el contenedor.

5. Todas las páginas en 500: el WAF de Cloudflare desafía al middleware

Síntoma. El contenedor arranca y queda healthy, /api/health responde 200, pero cualquier página devuelve 500. En el log del contenedor:

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
    at JSON.parse (<anonymous>)
    at async (.next/server/src/middleware.js…)

Evidencia. Ocurrió en el primer despliegue a producción (28/08/2026). El log del edge de Cloudflare para medusa.compulandia.com.py:

Campo Valor
ClientRequestPath /store/regions
ClientRequestUserAgent Next.js Middleware
ClientIP IP de egreso de la VM de GCP
EdgeResponseStatus 403
SecurityAction managedChallenge
SecurityRuleDescription manage definite bots
OriginResponseStatus 0 — nunca llegó a Medusa

Causa. El middleware hace fetch a /store/regions desde el servidor, con User-Agent Next.js Middleware y desde una IP de datacenter. El WAF gestionado lo clasifica como bot y le responde un managed challenge: una página HTML. Un fetch no puede resolver un desafío, así que recibe HTML donde esperaba JSON.

Por eso staging no fallaba: sale desde la red local, con otra reputación de IP.

Solución. Custom Rule con acción Skip en Security → WAF → Custom rules, por encima de las reglas gestionadas:

(ip.src eq <IP-de-egreso-de-la-VM> and http.host eq "medusa.compulandia.com.py")

Marcar Managed Rules y, si está activo, Super Bot Fight Mode. La IP de egreso es fija, así que la regla no debilita nada para el resto del tráfico.

Pendiente: /store/* también lo consume el navegador

El SDK corre además del lado del cliente —NEXT_PUBLIC_MEDUSA_BASE_URL va inlineada en el bundle—, así que el carrito, el checkout y la ficha de producto llaman a esa API desde el navegador del usuario. Si el WAF desafía esa ruta con criterio agresivo, rompe compras reales sin dejar rastro en ningún log del storefront.

Revisar en los eventos de seguridad si hay managedChallenge sobre /store/* con User-Agents de navegador. Si aparecen, la excepción debe ser por ruta y no solo por IP:

(http.host eq "medusa.compulandia.com.py" and starts_with(http.request.uri.path, "/store/"))

/store/* es una API pública cuya autenticación es la publishable key. Queda para el endurecimiento previo a la publicación del sitio.

Pendiente

  • Excepción del WAF para /store/* desde el navegador: ver la falla 5. Queda para el endurecimiento previo a la publicación.
  • REVALIDATE_SECRET es configuración inerte: ningún archivo del repositorio la lee, y el valor es supersecret, el placeholder del starter de Medusa, idéntico en los tres entornos. Si se implementa la revalidación bajo demanda, generar uno aleatorio por entorno (openssl rand -hex 32); si no, sacarla de los .env.
  • Host y procedimiento real de deploy: no hay evidencia en el repo de dónde corre el contenedor ni quién lo despliega. Completar con el dato del equipo.
  • Backups y restauración: no aplican a este repo (no tiene estado propio); dependen del backend Medusa.
  • Sin post-mortems registrados: las fallas de arriba salen del historial de git y del código, no de incidentes documentados. Cuando ocurra uno, va a docs/incidentes/AAAA-MM-tema.md.