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 used for user avatars, provides every user with an in-account Storage asset management page, 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 credentials, then paste them in Admin settings

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

Copy the access key, secret key, and endpoint into Admin → Runtime settings (/app/admin/settings). Runtime code does not read S3_* environment variables.

Configure the avatars bucket name

NEXT_PUBLIC_AVATARS_BUCKET_NAME

Paste the bucket name into Admin → Runtime settings. It is no longer an environment variable.

Image Proxy Prefix

  • Create a Worker and configure your own proxy route based on your deployment environment
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.

Asset Management Page

Every signed-in user gets a built-in page to browse, preview, download, and delete their own stored files.

Page Route

/app/settings/storage — requires login. Entry points: the account sidebar and the avatar dropdown menu.

API Endpoints

Four endpoints back the page. All are guarded by authMiddleware and mounted under the /assets base path (implementation: src/backend/api/routes/assets/):

EndpointDescription
GET /api/assetsLists the current user's files with pagination (default 50 per page, max 100)
GET /api/assets/usageReturns object count and total bytes (aggregation capped at 10,000 objects)
POST /api/assets/download-urlGenerates a presigned download URL (valid for 900 seconds)
POST /api/assets/deleteBatch delete (up to 200 keys per request, reports success/failure per key)

Security Model

  • Every operation is strictly scoped to the caller's own ${userId}/ prefix
  • Keys are validated against directory traversal; relative keys are capped at 512 characters
  • Preview URLs are built from config.storage.proxyUrls.avatarsFile — previews only work when NEXT_PUBLIC_AVATARS_PROXY_URL is configured
  • When S3 settings are missing in the database, the page shows a "storage not configured" state (error code STORAGE_ENV_MISSING)

UI Components

The UI lives in src/modules/settings/components/assets/: AssetManager, AssetTable, and AssetActionsMenu.

Required Storage Settings

S3 credentials and the avatars bucket live in Admin → Runtime settings. All three credential fields plus the bucket name must be set; otherwise the backend will not issue upload URLs:

SettingDescription
Access keyAccess Key for your S3-compatible service
Secret keySecret Key for your S3-compatible service
EndpointService endpoint (for example https://xxx.r2.cloudflarestorage.com)
Avatars bucketBucket used for avatar and file uploads

NEXT_PUBLIC_AVATARS_PROXY_URL stays in env. Client components (UserAvatar, AssetActionsMenu) build avatar URLs in the browser and cannot read the database.

When storage settings are missing:

  • Backend returns 503 with error code STORAGE_ENV_MISSING
  • Frontend shows a warning toast

storage section in src/config/index.ts:

ConfigDescription
storage.proxyUrls.avatarsFileAvatar access prefix domain (from NEXT_PUBLIC_AVATARS_PROXY_URL)