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

[
{
"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

S3_ACCESS_KEY_ID="55xxxc34b0"
S3_SECRET_ACCESS_KEY="08xxx625b1cc0cbe"
S3_ENDPOINT="https://xxx.r2.cloudflarestorage.com"Image Proxy Prefix
- Create a Worker, refer to Cloudflare R2 proxyUrls script for details
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:
- Frontend provides a relative path (
path), for example:2026_02_18_avatar_xxx.png - Backend adds a user directory prefix (
userId) as the final object key
Final key format: ${userId}/${path}
Example:
userId = user_abc123path = 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:
avatarUrlstarts withhttp: 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:
| Variable | Description |
|---|---|
S3_ACCESS_KEY_ID | Access Key for your S3-compatible service |
S3_SECRET_ACCESS_KEY | Secret Key for your S3-compatible service |
S3_ENDPOINT | Service endpoint (for example https://xxx.r2.cloudflarestorage.com) |
When missing:
- Backend returns
503with error codeSTORAGE_ENV_MISSING - Frontend shows a warning toast with the missing var name
Related Config Items
storage section in src/config/index.ts:
| Config | Description |
|---|---|
storage.meta | S3 server-side connection parameters |
storage.bucketNames.avatars | Avatar bucket (can be overridden by NEXT_PUBLIC_AVATARS_BUCKET_NAME) |
storage.proxyUrls.avatarsFile | Avatar access prefix domain (can be overridden by NEXT_PUBLIC_AVATARS_PROXY_URL) |
Related Docs
- Tech Stack: Object Storage — S3 compatibility overview
- User Settings — Profile and avatar setup


