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.
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:
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:
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.
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.