Skip to main content
Version: next

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 several default ones, 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.
  • Magento 1. With cms("magento1"), the Magento 1 connector registers a backend that stores media in your Magento instance — see Set up the Magento 1 backend.
  • Magento 2. With cms("magento2"), the Magento 2 connector registers a backend that stores media in your Magento instance — see Set up the Magento 2 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> {
/* … */
}
}

Declare unsupported operations

Some backends can't perform every operation. For example, a backend whose ids are derived from a file's path can't rename a file without breaking references already placed in pages. Rather than letting the editor call an operation that fails, list it in the optional unsupportedOperations property, and the media library hides the matching action:

app/cms/MyMediaBackend.ts
export class MyMediaBackend implements MediaBackend {
// List the operations your backend can't perform. Anything not listed is
// supported, so you only declare what your backend can't do.
readonly unsupportedOperations = ["renameFile", "renameFolder"];

// …
}

Hiding the action is a UI convenience, not a security boundary: keep the corresponding method rejecting a direct call (for example, throw) so the operation stays unavailable even if it's invoked outside the editor.

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.