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