Storage
Image and video uploads plus the per-user file browser — list, upload, move, delete and inspect files on the storage CDN.
Files live in object storage (Cloudflare R2) behind a CDN. Every user gets a private namespace —
all keys start with u/{userId}/ — and uploads come back as absolute CDN URLs (e.g.
https://storage.feeef.net/u/{userId}/…). Processed videos are served from
https://storage.feeef.org/videos/….
There are three surfaces:
| Surface | Base | Purpose |
|---|---|---|
| Image upload | /v1/services/storage/upload | Optimized image ingestion (resize, quality, format) |
| File browser | /v1/services/files | Media library: list, upload any type, move, delete, metadata |
| Video upload | /storage/upload (server root — see below) | Multi-quality video processing |
At a glance
Image upload
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /v1/services/storage/upload | Multipart image upload, returns { "url": "…" } | Bearer |
Multipart fields: file (required), folder, width, height, quality,
fit (cover, contain, fill, inside, outside). Limits: 20 MB, extensions
jpg jpeg png webp gif avif tiff svg. width/height below 300 are clamped up to 300. The
folder is always treated as relative and stored under u/{userId}/ (default
uploads/any) — don't pass a u/… prefix here.
File browser
The media library API. All paths are validated against your u/{userId}/ namespace; relative
folders are prefixed automatically on single upload.
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/services/files | List one directory level (path, recursive, paginationToken, limit) — returns files and directories | Bearer |
GET | /v1/services/files/objects | Flat object list, no directory grouping | Bearer |
POST | /v1/services/files | Upload one file (file + same image options as above) — returns url, path, name, type | Bearer |
POST | /v1/services/files/many | Upload many (files field); folder must be absolute (u/{userId}/…) | Bearer |
DELETE | /v1/services/files?path=… | Delete a file or a whole directory (recursive), returns deletedCount | Bearer |
DELETE | /v1/services/files/many?paths[]=… | Delete many paths, per-path results | Bearer |
GET | /v1/services/files/metadata?path=… | exists, size, contentType, lastModified | Bearer |
GET | /v1/services/files/size?path=… | Total size of a file or directory in bytes (bare number) | Bearer |
POST | /v1/services/files/move?source=…&destination=… | Move/rename a file or directory (copy + delete) | Bearer |
GET | /v1/files | Alias of GET /v1/services/files | Bearer |
GET | /v1/files/objects | Alias of GET /v1/services/files/objects | Bearer |
File-browser uploads accept up to 100 MB per file. Images
(jpg jpeg webp png gif avif tiff heic heif) are processed like the image endpoint (optimized,
renamed to a generated id); every other allowed type is stored as-is with its sanitized
filename:
| Category | Extensions |
|---|---|
| Documents | pdf doc docx txt csv xls xlsx |
| Archives | zip rar 7z tar gz |
| Video | mp4 webm mov avi mkv |
| Audio | mp3 wav ogg m4a aac |
Video upload
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /storage/upload | Multipart video upload; processed asynchronously into multiple qualities | Public |
Limits: 5 GB, extensions mp4 webm mov avi mkv. The response returns immediately while
processing continues in the background:
{
"id": "vid_abc123",
"url": "https://storage.feeef.org/videos/vid_abc123/metadata.json",
"status": "success"
}The metadata.json (and the quality renditions next to it) appear once processing finishes.
This route is registered at the server root, outside the versioned API group: it is not
available under https://api.feeef.org/v1/…. Call it on the path-prefixed backend origin
without the /v1 suffix (in local dev: http://localhost:3333/storage/upload). It currently
requires no authentication — treat it as unstable until it moves under /v1.
Examples
Upload an optimized image:
curl -X POST "https://api.feeef.org/v1/services/storage/upload" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-F "file=@banner.png" \
-F "folder=stores/STORE_ID/banners" \
-F "width=1200" -F "quality=80"Browse your media library:
curl -H "Authorization: Bearer $FEEEF_TOKEN" \
"https://api.feeef.org/v1/services/files?path=uploads/any&limit=100"Upload any file type through the file browser (here: a PDF):
curl -X POST "https://api.feeef.org/v1/services/files" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-F "file=@invoice.pdf" \
-F "folder=documents"Delete a file (or an entire directory) by path:
curl -X DELETE -H "Authorization: Bearer $FEEEF_TOKEN" \
"https://api.feeef.org/v1/services/files?path=u/USER_ID/documents/old.pdf"Upload a video for processing (path-prefixed origin, no /v1 — see the callout above):
curl -X POST "$API_ORIGIN/storage/upload" \
-F "file=@promo.mp4"Notes
- There is no temp-storage endpoint today. Uploads are permanent immediately: the backend
contains a temp-file registry and a
storage/clearcleanup route, but both are commented out. Remove unused files yourself via the file-browser delete endpoints. - Delete endpoints take paths in the query string (not the body), and directory deletes are
recursive — deleting
u/{userId}/documentsremoves everything under it. foldersemantics differ per endpoint: image upload always prefixesu/{userId}/; single file-browser upload prefixes relative folders;…/files/manyrejects folders that don't already start withu/{userId}.- The JavaScript SDK ships upload helpers only (
ff.storage.upload,uploadBytes,uploadStoreFile,uploadProductImage,uploadStoreLogo…); the file browser is not wrapped yet. The Dart SDK wraps both:Feeef.instance.storage(uploads) andFeeef.instance.files(list/objects/upload/uploadMany/delete/deleteMany/metadata/move). - Upload responses and stored objects are served from the storage CDN — URLs are permanent and publicly readable; treat them as unguessable but not access-controlled.