Saltar a contenido

ADR-0002 · Cloudflare Images como almacenamiento de medios, con hooks propios

  • Estado: aceptado
  • Decisores: no registrado en el repositorio (autoría del código: hquintero1)
  • Fecha de la decisión: no registrada. La guía de configuración (docs/integraciones/cloudflare-images.md) y src/utilities/cloudflare-images.ts datan de noviembre de 2025.

Contexto y problema

El template de Payload guarda los archivos subidos en disco (public/media/) y los procesa con sharp para generar los tamaños declarados en upload.imageSizes. Eso no sirve acá por dos motivos: el CMS corre en un contenedor efímero —el disco no sobrevive un redeploy— y las imágenes las consume un storefront externo, que necesita entrega por CDN y variantes responsivas sin que el CMS quede en el camino crítico de cada request.

Opciones consideradas

  1. Cloudflare Images con hooks propios en la colección media (elegida)
  2. Un plugin de almacenamiento oficial de Payload (@payloadcms/storage-s3 u otro) contra S3/R2
  3. Volumen persistente montado en el contenedor, sirviendo los archivos desde el propio CMS

Decisión

Cloudflare Images, integrado a mano. La colección media declara upload.disableLocalStorage: true y delega todo el ciclo de vida a tres hooks en src/collections/Media/hooks.ts:

  • beforeChange (solo en create) sube el buffer y persiste únicamente cfImageId, más filename, filesize y mimeType —requeridos para que el admin reconozca el documento como imagen—.
  • afterRead sintetiza url, thumbnailURL, baseDeliveryUrl y todo el grupo sizes a partir de cfImageId. Son campos virtuales: nunca se persisten.
  • beforeDelete borra la imagen remota pero se traga los errores, de modo que el borrado en Payload siempre procede.

Cloudflare Images hace almacenamiento, redimensionado y entrega por CDN en un solo servicio de costo plano, sin infraestructura propia. Se escribieron hooks en vez de usar un plugin de storage porque el modelo de Cloudflare Images no es un bucket de objetos: no hay clave de archivo que Payload pueda componer, sino un ID opaco más nombres de variante definidos fuera del repo.

Consecuencias

Positivas

  • El contenedor es efímero de verdad: no hay estado en disco que preservar.
  • Entrega por CDN con conversión automática a WebP/AVIF, sin costo de cómputo local.
  • Degradación controlada: sin credenciales de Cloudflare, la subida se saltea con un console.warn en lugar de tumbar el CMS.

Negativas / deuda asumida

  • Los campos de URL son virtuales. Cualquier consumidor que lea la tabla media directamente por SQL no ve url, sizes ni thumbnailURL: solo cfImageId. El acceso a medios tiene que pasar por la API.
  • Acoplamiento con configuración fuera del repo. Los siete nombres de IMAGE_VARIANTS deben existir textualmente en el dashboard de Cloudflare o las URLs dan 404, y nada en el build lo verifica.
  • upload.imageSizes se conserva en la colección solo por compatibilidad de forma: sharp nunca procesa estos archivos. Es una fuente de confusión viva.
  • Un borrado fallido en Cloudflare deja la imagen huérfana allá, en silencio.
  • Los hooks son código propio a mantener, sin la cobertura de un plugin oficial; ya requirió un script de reparación (scripts/fix-existing-media.ts) para documentos que quedaron sin filename.