Skip to content

[CMS-MIGRATE] Enable S3 Direct Client Upload in PayloadCMS Config #49

Description

@ASPactores

🎯 Objective

Target Feature: Direct Client Uploads for S3 Storage (@payloadcms/storage-s3)
Reference Component(s): src/payload.config.ts, @payloadcms/storage-s3


📌 Context & Motivation

Our Payload CMS is hosted on AWS Lambda behind AWS API Gateway.

Currently, media file uploads are proxied through the CMS backend (API Gateway → AWS Lambda → S3). This architecture introduces critical operational constraints:

  1. API Gateway Hard Payload Limit (6 MB): AWS API Gateway enforces an unconfigurable 6 MB payload limit on HTTP requests. Uploading any media file exceeding 6 MB returns a 413 Payload Too Large error.
  2. AWS Lambda Timeouts & Memory Overhead: Buffering large file binaries in Lambda memory and encoding/forwarding them to S3 increases invocation execution duration, memory consumption, and AWS costs.

Enabling direct client uploads (clientUploads: true) allows the browser to fetch a presigned S3 URL from Payload and upload file binaries directly to AWS S3, bypassing API Gateway and Lambda limits entirely.


🛠️ Technical Context

CMS Repository Notes

  • PayloadCMS S3 Plugin Docs: https://payloadcms.com/docs/upload/storage-adapters#s3-storage
  • Location: Modify src/payload.config.ts under the plugins array.
  • Configuration: Set clientUploads: true on the s3Storage plugin options to enable direct client-to-S3 file binary uploads via presigned URLs.
  • Environment Variables:
    • S3_BUCKET: Target S3 bucket name.
    • AWS_REGION: AWS region (default: ap-southeast-1).
    • CLOUDFRONT_DISTRIBUTION_DOMAIN: CDN domain for media URL resolution.
  • Proposed Configuration:
    plugins: [
      ...plugins,
      ...(process.env.S3_BUCKET
        ? [
            s3Storage({
              collections: {
                media: {
                  disableLocalStorage: true,
                  generateFileURL: ({ filename, prefix }) => {
                    const domain = process.env.CLOUDFRONT_DISTRIBUTION_DOMAIN
                    if (!domain) return filename
                    const normalizedDomain = domain.replace(/\/$/, '')
                    const protocol = normalizedDomain.startsWith('http') ? '' : 'https://'
                    return `${protocol}${normalizedDomain}/${prefix}/${filename}`
                  },
                  prefix: 'media',
                },
              },
              bucket: process.env.S3_BUCKET || '',
              config: {
                region: process.env.AWS_REGION || 'ap-southeast-1',
              },
              clientUploads: true,
              enabled: isProduction,
            }),
          ]
        : []),
    ]

Infrastructure / AWS Notes

  • S3 Bucket CORS Policy: Handled out-of-band by DevOps.

📋 Schema & Configuration Requirements

  • Option 1: clientUploads: true (Type: Boolean, Enabled: True on s3Storage plugin config in src/payload.config.ts)
  • Option 2: Presigned URL & CloudFront Compatibility (Verified for media collection)

🧪 Test Case Planning

[To be filled by Developer: As per our Developer Workflow, before writing code, reply to this issue with your proposed test cases (Admin UI, Validation, API Fetch, etc.). Wait for approval from the Project Lead.]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions