Media
Centralized asset management with upload handling, deduplication, and image transforms.
Media is a built-in entity (_media) — not a special system, just a collection with upload handling. All assets are centralized regardless of where they’re uploaded from.
Uploading files
POST /api/_media/upload
Content-Type: multipart/form-data
file: <binary>
alt: "Description of the image"
The response returns the created media record:
{
"data": {
"id": "uuid",
"filename": "photo.jpg",
"mimeType": "image/jpeg",
"size": 245000,
"width": 1920,
"height": 1080,
"alt": "Description of the image",
"url": "/api/assets/abc123.jpg",
"createdAt": "2025-06-01T00:00:00Z",
"updatedAt": "2025-06-01T00:00:00Z"
}
}
Image dimensions (width, height) are extracted automatically using sharp for supported image formats (JPEG, PNG, WebP, AVIF, TIFF, GIF). Non-image files and unreadable images will have null dimensions.
Image transforms
Uploaded images can be transformed on-the-fly by adding query parameters to the file URL:
GET /api/assets/abc123.jpg?w=400&format=webp
GET /api/assets/abc123.jpg?w=800&fit=cover&format=avif&q=75
Parameters
| Param | Description | Default |
|---|---|---|
w | Width in pixels (snapped to nearest allowed breakpoint). Height is calculated from aspect ratio. | Original |
fit | Resize strategy: cover, contain, fill, inside, outside | cover |
format | Output format: webp, avif, jpeg, png | webp |
q | Quality (1–100) | 80 |
Width is snapped to the nearest standard breakpoint (16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840) to prevent cache busting from arbitrary values. Images are never enlarged beyond their original dimensions.
Transformed images are cached to disk (in .cache/ inside the uploads directory) so repeated requests are served instantly without re-processing.
Without any query parameters, the original file is served as-is with immutable caching headers.
Deduplication
Files are deduplicated by SHA-256 hash. Uploading the same file twice returns the existing record instead of creating a duplicate. If you upload a duplicate with a different alt text, the alt text is updated on the existing record.
Referencing media
Use the media() field helper:
const posts = entity("posts", {
fields: [
text("title", { required: true }),
media("featuredImage"),
media("gallery", { multiple: true }),
],
});
Under the hood, media() creates a relation to the built-in _media collection. You can also use relation("image", { to: "_media" }) directly if you prefer.
In the admin UI, media fields automatically get a file upload UI with preview.
Storage adapters
Media storage is configured in cms.config.ts:
export default config({
storage: {
adapter: "local", // default
},
// ...
});
Local storage
Files are saved to a ./uploads/ directory and served via the API. This is the default and works without any additional configuration.
S3 / R2
S3-compatible storage adapters are planned but not yet implemented.
Querying media
Media is a regular collection, so you can query it like any other:
GET /api/_media?filter[mimeType][$startsWith]=image&sort=-createdAt&limit=20
Previous
Internationalization
Next
Admin Extensibility