⚠️ Work in progress — createCMS is pre-1.0 and not production-ready (not tested in production). Expect breaking changes.
createCMS
Guides

Upload and serve media

Configure S3 storage, upload assets from the browser, and serve them.

This guide uploads and serves assets. It assumes a CMS instance (see Quickstart) and an S3-compatible bucket. For the model behind it, see Media.

Configure storage

createCMS requires a media config: your bucket plus a public base URL (a CDN domain). The full shape is in the configuration reference:

lib/cms.ts
media: {
  provider: 'cloudflare',
  accountId: process.env.R2_ACCOUNT_ID!,
  bucketName: process.env.R2_BUCKET!,
  accessKeyId: process.env.R2_KEY!,
  secretAccessKey: process.env.R2_SECRET!,
  publicUrl: process.env.R2_PUBLIC_URL!,
},

Upload from the browser

The React client's media.useUploadAssets() returns an upload function plus progress state. Under the hood it requests signed URLs and PUTs each file straight to your bucket:

components/uploader.tsx
'use client';
import { cmsClient } from '@/lib/cms-client';

export function Uploader() {
  const { upload, isUploading, totalProgress } = cmsClient.media.useUploadAssets();

  return (
    <>
      <input
        type="file"
        multiple
        onChange={(e) => upload(Array.from(e.target.files ?? []))}
      />
      {isUploading ? <p>Uploading {totalProgress}%</p> : null}
    </>
  );
}

upload(files, options?) takes a File[] and an optional { folderId }. Uploads are validated server-side against the limits (defaults: maxFiles 10, maxFileSize 4 MB, allowedMimeTypes ['image/*', 'video/*', 'application/pdf']); a rejected file surfaces as FILE_TOO_LARGE, TOO_MANY_FILES, or INVALID_FILE_TYPE. New assets are created private.

FieldTypeDescription
upload(files: File[], options?: { folderId?: string }) => Promise<void>Start an upload.
isUploadingbooleanWhether an upload is in progress.
totalProgressnumberOverall progress, 0 to 100.
filesMediaUploadFileState[]Per-file name, progress, status, and result ({ id, slug, objectKey }).
abort / reset() => voidCancel or clear.

Optimize before upload

Add the media optimize plugin to resize and convert in the browser first. useOptimize returns OptimizeResult[] ({ file, originalVariant?, optimized }); flatten it to the File[] that upload expects:

const { results } = cmsClient.optimize.useOptimize(file);
const files = (results ?? []).flatMap((r) =>
  r.originalVariant ? [r.file, r.originalVariant] : [r.file],
);
// cmsClient.media.useUploadAssets().upload(files)

Serve an asset

Serve assets through the CMS gate: GET /media/asset/{id}, a public redirect to the object, addressed by the asset id (which is exactly what content stores). Use this in content and on public pages — it enforces status (a private asset returns ASSET_ACCESS_DENIED; flip one to public with updateAssetStatus) and honors ?format= and ?w= by resolving a pre-uploaded variant asset (falling back to the original). There is no on-the-fly transform: produce variants ahead of time (for example with the optimize plugin).

<img src="https://your-cms.example.com/media/asset/ast_6t4x5a98n2io352uhbkq" />

listAssets and the upload responses also return a direct url per asset — your publicUrl joined with the asset's objectKey. It bypasses the gate (no status check, no transforms, ignores query strings), so it is for internal tooling like a media-library admin UI showing thumbnails of assets you manage — not for embedding in content:

const thumb = asset.url; // direct object URL — admin/library display only
// e.g. https://cdn.example.com/welcome-hero

Upload from the server

For server-initiated uploads, call the API directly. createSignedUpload returns presigned PUT URLs (you then PUT the bytes with the returned headers); uploadAssets sends the bytes through the server (each file needs a buffer: Blob | ArrayBuffer):

const { assets } = await cms.api.media.createSignedUpload({
  body: { files: [{ name: 'hero.jpg', size: file.size, type: 'image/jpeg' }] },
});
// PUT the file to assets[0].signedUrl using assets[0].headers

For the asset model, folders, and private vs public, see Media.

On this page