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

ParamDescriptionDefault
wWidth in pixels (snapped to nearest allowed breakpoint). Height is calculated from aspect ratio.Original
fitResize strategy: cover, contain, fill, inside, outsidecover
formatOutput format: webp, avif, jpeg, pngwebp
qQuality (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