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) ysrc/utilities/cloudflare-images.tsdatan 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¶
- Cloudflare Images con hooks propios en la colección
media(elegida) - Un plugin de almacenamiento oficial de Payload (
@payloadcms/storage-s3u otro) contra S3/R2 - 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 encreate) sube el buffer y persiste únicamentecfImageId, másfilename,filesizeymimeType—requeridos para que el admin reconozca el documento como imagen—.afterReadsintetizaurl,thumbnailURL,baseDeliveryUrly todo el gruposizesa partir decfImageId. Son campos virtuales: nunca se persisten.beforeDeleteborra 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.warnen lugar de tumbar el CMS.
Negativas / deuda asumida¶
- Los campos de URL son virtuales. Cualquier consumidor que lea la tabla
mediadirectamente por SQL no veurl,sizesnithumbnailURL: solocfImageId. El acceso a medios tiene que pasar por la API. - Acoplamiento con configuración fuera del repo. Los siete nombres de
IMAGE_VARIANTSdeben existir textualmente en el dashboard de Cloudflare o las URLs dan 404, y nada en el build lo verifica. upload.imageSizesse conserva en la colección solo por compatibilidad de forma:sharpnunca 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 sinfilename.