MuseMVP Docs

S3 Object Storage

Guide to Cloudflare R2/S3 CORS, credential variables, signed upload URLs, and avatar proxy access configuration.

MuseMVP wraps a generic S3-compatible file upload API that works with AWS S3, Cloudflare R2, or self-hosted MinIO. It is currently used for user avatars and can be extended for other file types.

Setup Steps

Configure CORS

Configure CORS

[
  {
    "AllowedOrigins": [
      "*"
    ],
    "AllowedMethods": [
      "GET",
      "POST",
      "PUT",
      "DELETE",
      "HEAD"
    ],
    "AllowedHeaders": [
      "*"
    ],
    "ExposeHeaders": [
      "ETag"
    ],
    "MaxAgeSeconds": 3600
  }
]

Tip

CORS configuration allows the frontend to access your Cloudflare R2 bucket. Adjust it for your actual requirements, especially AllowedOrigins.

Create S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_ENDPOINT

Create Access Key and Secret Key Create Access Key and Secret Key Create Access Key and Secret Key Create Access Key and Secret Key Create Access Key and Secret Key

S3_ACCESS_KEY_ID="55xxxc34b0"
S3_SECRET_ACCESS_KEY="08xxx625b1cc0cbe"
S3_ENDPOINT="https://xxx.r2.cloudflarestorage.com"

Configure NEXT_PUBLIC_AVATARS_BUCKET_NAME

NEXT_PUBLIC_AVATARS_BUCKET_NAME

NEXT_PUBLIC_AVATARS_BUCKET_NAME="musemvp-demo"

Image Proxy Prefix

export const config = {
  storage: {
    proxyUrls: {
      avatarsFile: resolveUrlValue(
        process.env.NEXT_PUBLIC_AVATARS_PROXY_URL,
        "",
      ),
    },
  },
};

Upload Object Path Rules

Avatar uploads use a two-level path structure:

  1. Frontend provides a relative path (path), for example: 2026_02_18_avatar_xxx.png
  2. Backend adds a user directory prefix (userId) as the final object key

Final key format: ${userId}/${path}

Example:

  • userId = user_abc123
  • path = 2026_02_18_avatar_3f2c...png
  • Object storage key = user_abc123/2026_02_18_avatar_3f2c...png

Each user's static assets are stored under their own userId directory, making retrieval, migration, and cleanup easier by user scope.

Proxy Prefix and Access URL

UserAvatar handles two cases:

  • avatarUrl starts with http: use as-is (full external URL)
  • Otherwise: concatenate ${config.storage.proxyUrls.avatarsFile}/${avatarUrl}

Recommended Setup

Set config.storage.proxyUrls.avatarsFile (or NEXT_PUBLIC_AVATARS_PROXY_URL) to your CDN or object storage public read URL, e.g. https://static.example.com.

Required Storage Environment Variables

All three must be set; otherwise the backend will not issue upload URLs:

VariableDescription
S3_ACCESS_KEY_IDAccess Key for your S3-compatible service
S3_SECRET_ACCESS_KEYSecret Key for your S3-compatible service
S3_ENDPOINTService endpoint (for example https://xxx.r2.cloudflarestorage.com)

When missing:

  • Backend returns 503 with error code STORAGE_ENV_MISSING
  • Frontend shows a warning toast with the missing var name

storage section in src/config/index.ts:

ConfigDescription
storage.metaS3 server-side connection parameters
storage.bucketNames.avatarsAvatar bucket (can be overridden by NEXT_PUBLIC_AVATARS_BUCKET_NAME)
storage.proxyUrls.avatarsFileAvatar access prefix domain (can be overridden by NEXT_PUBLIC_AVATARS_PROXY_URL)