File upload
src/upload/ provides image upload for agent icons, screenshots, and avatars.
Endpoint
POST /upload — multipart/form-data, single file field. Returns:
{
"data": { "url": "https://cdn.example.com/agents/icon-abc.png" }
}
Storage providers
The module is provider-agnostic. The current production deployment uses S3 / R2 / Cloudflare Images depending on environment. Provider selection lives in the upload service's constructor, driven by env vars like STORAGE_PROVIDER, S3_BUCKET, etc.
To swap providers, edit the upload service and add the new SDK + env vars. Don't sprinkle provider-specific code in callers.
Validation
| Check | Constraint |
|---|---|
| MIME type | image/png, image/jpeg, image/webp, image/svg+xml |
| Size | Default 5 MB (configurable via env) |
| Filename | Random UUID-based, never user-controlled |
Reject everything else with 422 UNPROCESSABLE_ENTITY.
Usage from frontend
fleapo-marketplace/src/api/upload/ is generated by Orval. Wrap it with a hook in the admin form so the URL flows back into the agent's iconUrl / photoUrls[] automatically.
Public URLs
All uploads are publicly readable. The marketplace UI loads them via <img src>, so the storage bucket is fronted by a CDN with public-read.
If you need authenticated assets (e.g. signed contracts), build a separate module that returns short-lived signed URLs.
Garbage collection
No automatic cleanup. Replacing an agent's icon leaves the old object stranded. Build scripts/gc-uploads.ts when this becomes a cost issue: walk agents.iconUrl + photoUrls + capabilities.icon, collect referenced object keys, delete unreferenced keys.
Why a separate module
Keeping uploads in one place makes future improvements (image resizing, format conversion, CDN URL rewriting) a single-file change.