Skip to main content
Version: 3.x

Customize the media backend

Where the CMS stores and serves media, how to configure it, and how to plug a custom media backend for your own storage.

The media library is backed by a pluggable media backend. @front-commerce/cms ships two, and you can provide your own to store media wherever you need.

Where media is stored and served

  • Filesystem (default). Out of the box, media is stored on the local filesystem under .front-commerce/cms-fs. This needs no backend and is handy for local development.
  • Gezy. With cms("gezy"), the Gezy connector registers a backend that proxies the Gezy CMS media API, so media lives in your Gezy instance — see Set up the Gezy backend.

Whichever backend is active, binaries are served through the public route /cms/media/<id> (images are resized on the fly and cached). Each file's URL is built against that prefix, so a value stored in a page stays valid even if you later swap the backend.

You can force the filesystem backend — ignoring any connector-provided one — with FRONT_COMMERCE_CMS_MEDIA_DANGEROUSLY_FORCE_FILESYSTEM=true (see the Gezy setup). As the name says, it's a development-only escape hatch.

Write a custom media backend

A backend implements the MediaBackend interface from @front-commerce/cms/media — the operations the editor's media library calls: browsing a folder, searching, uploading, renaming, deleting, and reading a file's binary.

app/cms/MyMediaBackend.ts
import type {
MediaBackend,
ServerMediaFolderResult,
ServerMediaSearchResults,
MediaSearchScope,
ServerMediaFile,
ServerMediaFolderRef,
MediaFile,
ReadFileOptions,
} from "@front-commerce/cms/media";
import { MEDIA_ROOT_ID, MEDIA_URL_PREFIX } from "@front-commerce/cms/media";

export class MyMediaBackend implements MediaBackend {
// Surface your top-level folder under `MEDIA_ROOT_ID` ("/") and build each
// file's `url` as `${MEDIA_URL_PREFIX}/<id>` so values stay portable.
getFolder(id: string | null): Promise<ServerMediaFolderResult | null> {
/* … */
}
search(
query: string,
scope: MediaSearchScope
): Promise<ServerMediaSearchResults> {
/* … */
}
uploadFile(file: File, parentFolderId: string): Promise<ServerMediaFile> {
/* … */
}
deleteFile(id: string): Promise<void> {
/* … */
}
deleteFolder(id: string): Promise<void> {
/* … */
}
renameFile(id: string, name: string): Promise<ServerMediaFile> {
/* … */
}
renameFolder(id: string, name: string): Promise<ServerMediaFolderRef> {
/* … */
}
createFolder(
parentFolderId: string,
name: string
): Promise<ServerMediaFolderRef> {
/* … */
}
// Returns the binary served by the `/cms/media/<id>` route.
readFile(id: string, options?: ReadFileOptions): Promise<MediaFile | null> {
/* … */
}
}

Register the backend

Register your backend from your extension's onServerServicesInit hook, on the cms service:

your extension's index.ts
import { defineRemixExtension } from "@front-commerce/remix";
import { MyMediaBackend } from "./app/cms/MyMediaBackend";

export default function myConnector() {
return defineRemixExtension({
meta: import.meta,
name: "my-connector",
unstable_lifecycleHooks: {
onServerServicesInit: async (services) => {
services.DI.get("cms").registerMediaBackend(new MyMediaBackend());
},
},
});
}

This extension must be registered after cms(...) in your front-commerce.config.ts, because it reads the cms service that cms(...) sets up at startup.

note

One backend per app. registerMediaBackend accepts a single override — registering a second one throws. And if FRONT_COMMERCE_CMS_MEDIA_DANGEROUSLY_FORCE_FILESYSTEM is set, the registration is ignored and the filesystem backend stays active.