Migrar Payload CMS de AWS S3 a Supabase Storage
Si usas Payload CMS 3 con el plugin @payloadcms/storage-s3, seguramente lo tengas apuntando a AWS S3. Migrar a Supabase Storage no implica cambiar de plugin: Supabase expone una API S3-compatible, así que basta con apuntar el cliente al endpoint correcto y ajustar dos detalles que la gente suele olvidar.
Por qué migrar
Yo lo hice por tres razones prácticas. Primero, la base de datos ya estaba en Supabase (Postgres), así que tener media en el mismo proveedor simplifica facturación y monitoring. Segundo, la consola de Storage de Supabase es más amigable que la de S3 para ver, renombrar y borrar archivos puntuales. Tercero, el tier gratuito de Supabase cubre sin problema un sitio personal.
El costo es que Supabase Storage no tiene paridad total con S3 — no soporta virtual-hosted-style URLs ni ciertas APIs avanzadas. Para un CMS de blog no es problema, pero si usas features como multipart uploads muy grandes o lifecycle policies, revisa la documentación antes.
El cambio en payload.config.ts
El plugin s3Storage acepta cualquier cliente compatible. Dos ajustes obligatorios:
[ts]import { s3Storage } from '@payloadcms/storage-s3'
const MEDIA_PREFIX = 'media'
const S3_PUBLIC_URL = process.env.S3_PUBLIC_URL.replace(/\/$/, '')
s3Storage({
collections: {
media: {
prefix: MEDIA_PREFIX,
generateFileURL: ({ filename, prefix }) => {
if (!filename) return ''
const key = prefix ? `${prefix.replace(/\/$/, '')}/${filename}` : filename
return `${S3_PUBLIC_URL}/${key}`
},
},
},
bucket: process.env.S3_BUCKET,
config: {
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || 'us-east-1',
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
},
})Los dos detalles críticos:
forcePathStyle: true. Supabase no soporta la forma virtual-hosted (<bucket>.endpoint/...). Sin este flag, el SDK de AWS construye URLs incorrectas y falla con errores vagos de DNS o 403. Es el 80% de los bugs al migrar.generateFileURLdebe incluir el prefix. Si subes conprefix: 'media/'pero construyes la URL pública sin ese prefix, obtendrás 404 en cada imagen.
Variables de entorno
[bash]S3_BUCKET=mi-bucket
S3_ENDPOINT=https://<project-ref>.supabase.co/storage/v1/s3
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=<from Supabase dashboard>
S3_SECRET_ACCESS_KEY=<from Supabase dashboard>
S3_PUBLIC_URL=https://<project-ref>.supabase.co/storage/v1/object/public/mi-bucketLos access keys no son el anon ni el service_role. Se generan en el dashboard de Supabase en Storage → Settings → S3 Access Keys, y son específicos para la API S3. Un par de keys basta para todo tu proyecto.
El bucket debe ser público
Si quieres que las URLs generadas por generateFileURL funcionen sin firmar, el bucket debe estar marcado como public. En Supabase: Storage → el bucket → Settings → "Public bucket" en ON.
Si por política no puedes usar bucket público, el plugin soporta firmar URLs consignedDownloads, pero tendrás que implementargenerateFileURLmanualmente para devolver una URL firmada por cada request, y Next Image no las cachea tan bien.
Next.js: permitir el host
Next Image rechaza hosts no declarados. Añade en next.config.mjs:
[js]images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.supabase.co',
pathname: '/storage/v1/object/public/**',
},
],
}El wildcard *.supabase.co cubre cualquier proyecto que tengas, útil si mueves entre staging y producción.
Migrar archivos existentes
Payload guarda solo el filename en la DB y aplica generateFileURL al leer. Eso significa que al cambiar la base estás apuntando inmediatamente a Supabase, aunque los archivos sigan físicamente en AWS. Para no romper las imágenes antiguas, cópialas primero al bucket de Supabase manteniendo el prefix:
[bash]aws s3 sync s3://mi-bucket-antiguo/media/ \
s3://mi-bucket-nuevo/media/ \
--endpoint-url https://<project-ref>.supabase.co/storage/v1/s3 \
--region us-east-1Testea con un par de imágenes primero. Una vez verificado, puedes hacer el sync completo y actualizar el payload.config.ts.
Checklist final
forcePathStyle: trueen el config.generateFileURLrespeta elprefix.- Access keys S3 (no service-role) en el
.env. - Bucket marcado como público en Supabase.
*.supabase.coenremotePatternsdenext.config.- Archivos antiguos copiados al nuevo bucket antes del cambio.
Con esas seis cosas la migración es un cambio de tres líneas de código y cero downtime.
[ TAGS ]

