From b1ee204369e862a84d0c81167d4e03509a73c265 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 19 Aug 2025 12:59:15 +0300 Subject: [PATCH] docs: integrate payload guide (#13162) * docs: integrate payload guide * remove todo * fix vale errors * change items per row in integrations page * updates to guide --- www/apps/book/public/llms-full.txt | 3170 +++++++++++++++ .../app/integrations/guides/payload/page.mdx | 3433 +++++++++++++++++ www/apps/resources/app/integrations/page.mdx | 9 + www/apps/resources/generated/edit-dates.mjs | 3 +- www/apps/resources/generated/files-map.mjs | 4 + .../generated-commerce-modules-sidebar.mjs | 8 + .../generated-how-to-tutorials-sidebar.mjs | 9 + .../generated-integrations-sidebar.mjs | 8 + .../resources/sidebars/how-to-tutorials.mjs | 7 + www/apps/resources/sidebars/integrations.mjs | 5 + www/packages/tags/src/tags/product.ts | 4 + www/packages/tags/src/tags/server.ts | 4 + www/packages/tags/src/tags/tutorial.ts | 4 + 13 files changed, 6667 insertions(+), 1 deletion(-) create mode 100644 www/apps/resources/app/integrations/guides/payload/page.mdx diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index ee22b31185..112714d0d0 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -73448,6 +73448,3175 @@ If you encounter issues not covered in the troubleshooting guides: 3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. +# Integrate Payload CMS with Medusa + +In this tutorial, you'll learn how to integrate [Payload](https://payloadcms.com/) with Medusa. + +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa also facilitates integrating third-party services that enrich your application with features specific to your unique business use case. + +By integrating Payload, you can manage your products' content with powerful content management capabilities, such as managing custom fields, media, localization, and more. + +## Summary + +By following this tutorial, you'll learn how to: + +- Install and set up Medusa. +- Set up Payload in the Next.js Starter Storefront. +- Integrate Payload with Medusa to sync product data. + - You'll sync product data when triggered manually by admin users, or as a result of product events in Medusa. +- Display product data from Payload in the Next.js Starter Storefront. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +![Diagram showcasing a flowchart of interactions between customer, admin, Medusa, and Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754551568/Medusa%20Resources/payload-summary_ndeiw0.jpg) + +[Full Code](https://github.com/medusajs/examples/tree/main/payload-integration): Find the full code of the guide in this repository. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes." + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Set Up Payload in the Next.js Starter Storefront + +In this step, you'll set up Payload in the Next.js Starter Storefront. This requires installing the necessary dependencies, configuring Payload, and creating collections for products and other types. + +### a. Install Dependencies + +In the directory of the Next.js Starter Storefront, run the following command to install the necessary dependencies: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm install payload @payloadcms/next @payloadcms/richtext-lexical sharp @payloadcms/db-postgres +``` + +### b. Add Resolution for `undici` + +Payload uses the `undici` package, but some versions of it cause an error in the Payload CLI. + +To avoid these errors, add the following resolution and override to the `package.json` file of the Next.js Starter Storefront: + +```json title="package.json" badgeLabel="Storefront" badgeColor="blue" +{ + "resolutions": { + // other resolutions... + "undici": "5.20.0" + }, + "overrides": { + // other overrides... + "undici": "5.20.0" + } +} +``` + +Then, re-install the dependencies to ensure the correct version of `undici` is used: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm install +``` + +### c. Copy Payload Template Files + +Next, you'll need to copy the Payload template files into the Next.js Starter Storefront. These files allow you to access the Payload admin from the Next.js Starter Storefront. + +You can find the files in the [examples GitHub repository](https://github.com/medusajs/examples/tree/main/payload-integration/storefront/src/app/\(payload\)). Copy these files into a new `src/app/(payload)` directory in the Next.js Starter Storefront. + +Then, move all previous files that were under the `src/app` directory into a new `src/app/(storefront)` directory. This will ensure that the Payload admin is accessible at the `/admin` route, and the storefront is still accessible at the root route. + +So, the `src/app` directory should now only include the `(payload)` and `(storefront)` directories, each containing their respective files. + +![Overview of the Next.js Starter Storefront directory structure of the src directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1754474683/Medusa%20Resources/payload-storefront-dir_lt7yyw.jpg) + +### d. Modify Next.js Middleware + +The Next.js Starter Storefront uses a middleware to prefix all route paths with the first region's country code. While this is useful for storefront routes, it's unnecessary for the Payload admin routes. + +So, you'll modify the middleware to exclude the `/admin` routes. + +In `src/middleware.ts`, change the `config` object to include `/admin` in the `matcher` regex pattern: + +```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" +export const config = { + matcher: [ + "/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp|admin).*)", + ], +} +``` + +### e. Add Payload Configuration + +Next, you'll add the necessary configuration to run Payload in the Next.js Starter Storefront. + +Create the file `src/payload.config.ts` with the following content: + +```ts title="src/payload.config.ts" badgeLabel="Storefront" badgeColor="blue" +import sharp from "sharp" +import { lexicalEditor } from "@payloadcms/richtext-lexical" +import { postgresAdapter } from "@payloadcms/db-postgres" +import { buildConfig } from "payload" + +export default buildConfig({ + editor: lexicalEditor(), + collections: [ + // TODO add collections + ], + + secret: process.env.PAYLOAD_SECRET || "", + db: postgresAdapter({ + pool: { + connectionString: process.env.PAYLOAD_DATABASE_URL || "", + }, + }), + sharp, +}) +``` + +The configurations are mostly default Payload configurations. You configure Payload to use PostgreSQL as the database adapter. Later, you'll add collections for products and other types. + +Refer to the [Payload documentation](https://payloadcms.com/docs/configuration/overview) for more information on configuring Payload. + +In the configurations, you use two environment variables. To set them, add the following in your storefront's `.env.local` file: + +```shell title=".env.local" badgeLabel="Storefront" badgeColor="blue" +PAYLOAD_DATABASE_URL=postgres://postgres:@localhost:5432/payload +PAYLOAD_SECRET=supersecret +``` + +Where: + +- `PAYLOAD_DATABASE_URL` is the connection string to the PostgreSQL database that Payload will use. You don't need to create the database beforehand, as Payload will create it automatically. +- `PAYLOAD_SECRET` is your Payload secret. In production, you should use a complex and secure string. + +You also need to add a path alias to the `payload.config.ts` file, as Payload will try to import it using `@payload-config`. + +In `tsconfig.json`, add the following path alias: + +```json title="tsconfig.json" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"]]} +{ + "compilerOptions": { + // other options... + "paths": { + // other paths... + "@payload-config": ["./payload.config.ts"] + } + } +} +``` + +The `baseUrl` in the `tsconfig.json` file is set to `"./src"`, so the path alias will resolve to `src/payload.config.ts`. + +### f. Customize Next.js Configurations + +You also need to customize the Next.js configurations to ensure that Payload works correctly with the Next.js Starter Storefront. + +In `next.config.js`, add the following `require` statement at the top of the file: + +```js title="next.config.js" badgeLabel="Storefront" badgeColor="blue" +const { withPayload } = require("@payloadcms/next/withPayload") +``` + +Then, find the `module.exports` statement and replace it with the following: + +```js title="next.config.js" badgeLabel="Storefront" badgeColor="blue" +module.exports = withPayload(nextConfig) +``` + +You wrap the Next.js configuration with the `withPayload` function to ensure that Payload works correctly with Next.js. + +### g. Add Collections to Payload + +Now that Payload is set up in your storefront, you'll create the following [collections](https://payloadcms.com/docs/configuration/collections): + +- `User`: A Payload user with API key authentication, allowing you later to sync product data from Medusa to Payload. +- `Media`: A collection for media files, allowing you to manage product images and other media. +- `Product`: A collection for products, which will be synced with Medusa's product data. + +#### User Collection + +To create the `User` collection, create the file `src/collections/Users.ts` with the following content: + +```ts title="src/collections/Users.ts" badgeLabel="Storefront" badgeColor="blue" +import type { CollectionConfig } from "payload" + +export const Users: CollectionConfig = { + slug: "users", + admin: { + useAsTitle: "email", + }, + auth: { + useAPIKey: true, + }, + fields: [], +} +``` + +The `Users` collection allows you to manage users that can log into the Payload admin with email and API key authentication. + +Refer to the [Payload documentation](https://payloadcms.com/docs/authentication/api-keys) to learn more about API key authentication. + +#### Media Collection + +To create the `Media` collection, create the file `src/collections/Media.ts` with the following content: + +```ts title="src/collections/Media.ts" badgeLabel="Storefront" badgeColor="blue" +import { CollectionConfig } from "payload" + +export const Media: CollectionConfig = { + slug: "media", + upload: { + staticDir: "public", + imageSizes: [ + { + name: "thumbnail", + width: 400, + height: 300, + position: "centre", + }, + { + name: "card", + width: 768, + height: 1024, + position: "centre", + }, + { + name: "tablet", + width: 1024, + height: undefined, + position: "centre", + }, + ], + adminThumbnail: "thumbnail", + mimeTypes: ["image/*"], + pasteURL: { + allowList: [ + { + protocol: "http", + hostname: "localhost", + }, + { + protocol: "https", + hostname: "medusa-public-images.s3.eu-west-1.amazonaws.com", + }, + { + protocol: "https", + hostname: "medusa-server-testing.s3.amazonaws.com", + }, + { + protocol: "https", + hostname: "medusa-server-testing.s3.us-east-1.amazonaws.com", + }, + ], + }, + }, + fields: [ + { + name: "alt", + type: "text", + label: "Alt Text", + required: false, + }, + ], +} +``` + +The `Media` collection will store media files, such as product images. You can upload files to the [Storage Adapters](https://payloadcms.com/docs/upload/storage-adapters) configured in Payload, such as AWS S3 or local storage. The above configurations point to the `public` directory of the Next.js Starter Storefront as the upload directory. + +Note that you allow pasting URLs from specific sources, such as the Medusa public images S3 bucket. This allows you to paste Medusa's stock image URLs in the Payload admin. + +#### Product Collection + +Finally, you'll add the `Product` collection, which will be synced with Medusa's product data. + +Create the file `src/collections/Products.ts` with the following content: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={productCollectionHighlights} +import { CollectionConfig } from "payload" +import { payloadMedusaSdk } from "../lib/payload-sdk" + +export const Products: CollectionConfig = { + slug: "products", + admin: { + useAsTitle: "title", + }, + fields: [ + { + name: "medusa_id", + type: "text", + label: "Medusa Product ID", + required: true, + unique: true, + admin: { + description: "The unique identifier from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "title", + type: "text", + label: "Title", + required: true, + admin: { + description: "The product title", + }, + }, + { + name: "handle", + type: "text", + label: "Handle", + required: true, + admin: { + description: "URL-friendly unique identifier", + }, + validate: (value: any) => { + // validate URL-friendly handle + if (typeof value !== "string") { + return "Handle must be a string" + } + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) { + return "Handle must be URL-friendly (lowercase letters, numbers, and hyphens only)" + } + return true + }, + }, + { + name: "subtitle", + type: "text", + label: "Subtitle", + required: false, + admin: { + description: "Product subtitle", + }, + }, + { + name: "description", + type: "richText", + label: "Description", + required: false, + admin: { + description: "Detailed product description", + }, + }, + { + name: "thumbnail", + type: "upload", + relationTo: "media" as any, + label: "Thumbnail", + required: false, + admin: { + description: "Product thumbnail image", + }, + }, + { + name: "images", + type: "array", + label: "Product Images", + required: false, + fields: [ + { + name: "image", + type: "upload", + relationTo: "media" as any, + required: true, + }, + ], + admin: { + description: "Gallery of product images", + }, + }, + { + name: "seo", + type: "group", + label: "SEO", + fields: [ + { + name: "meta_title", + type: "text", + label: "Meta Title", + required: false, + }, + { + name: "meta_description", + type: "textarea", + label: "Meta Description", + required: false, + }, + { + name: "meta_keywords", + type: "text", + label: "Meta Keywords", + required: false, + }, + ], + admin: { + description: "SEO-related fields for better search visibility", + }, + }, + { + name: "options", + type: "array", + fields: [ + { + name: "title", + type: "text", + label: "Option Title", + required: true, + }, + { + name: "medusa_id", + type: "text", + label: "Medusa Option ID", + required: true, + admin: { + description: "The unique identifier for the option from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + ], + validate: (value: any, { req, previousValue }) => { + // TODO add validation to ensure that the number of options cannot be changed + }, + }, + { + name: "variants", + type: "array", + fields: [ + { + name: "title", + type: "text", + label: "Variant Title", + required: true, + }, + { + name: "medusa_id", + type: "text", + label: "Medusa Variant ID", + required: true, + admin: { + description: "The unique identifier for the variant from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "option_values", + type: "array", + fields: [ + { + name: "medusa_id", + type: "text", + label: "Medusa Option Value ID", + required: true, + admin: { + description: "The unique identifier for the option value from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "medusa_option_id", + type: "text", + label: "Medusa Option ID", + required: true, + admin: { + description: "The unique identifier for the option from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "value", + type: "text", + label: "Value", + required: true, + }, + ], + }, + ], + validate: (value: any, { req, previousValue }) => { + // TODO add validation to ensure that the number of variants cannot be changed + }, + }, + ], + hooks: { + // TODO add + }, + access: { + create: ({ req }) => !!req.query.is_from_medusa, + delete: ({ req }) => !!req.query.is_from_medusa, + }, +} +``` + +You create a `Products` collection having the following fields: + +- `medusa_id`: The product's ID in Medusa, which is useful when syncing data between Payload and Medusa. +- `title`: The product's title. +- `handle`: A URL-friendly unique identifier for the product. +- `subtitle`: An optional subtitle for the product. +- `description`: A rich text description of the product. +- `thumbnail`: An optional thumbnail image for the product. +- `images`: An array of images for the product. +- `seo`: A group of fields for SEO-related information, such as meta title, description, and keywords. +- `options`: An array of product options, such as size or color. +- `variants`: An array of product variants, each with its own title and option values. + +All of these fields will be filled from Medusa, and will be synced back to Medusa when the product is updated in Payload. + +In addition, you also add the following [access-control](https://payloadcms.com/docs/access-control/overview) configurations: + +- You disallow creating or deleting products from the Payload admin, as these actions should only be performed from Medusa. +- You disallow updating the `medusa_id` fields from the Payload admin, as these fields are managed by Medusa. + +#### Add Validation for Options and Variants + +Payload admin users can only manage the content of product options and variants, but they shouldn't be able to remove or add new options or variants. + +To ensure this behavior, you'll add validation to the `options` and `variants` fields in the `Products` collection. + +First, replace the `validate` function in the `options` field with the following: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +export const Products: CollectionConfig = { + // other configurations... + fields: [ + // other fields... + { + name: "options", + // other configurations... + validate: (value: any, { req, previousValue }) => { + if (req.query.is_from_medusa) { + return true // Skip validation if the request is from Medusa + } + + if (!Array.isArray(value)) { + return "Options must be an array" + } + + const optionsChanged = value.length !== previousValue?.length || value.some((option) => { + return !option.medusa_id || !previousValue?.some( + (prevOption) => (prevOption as any).medusa_id === option.medusa_id + ) + }) + + // Prevent update if the number of options is changed + return !optionsChanged || "Options cannot be changed in number" + }, + }, + ], +} +``` + +If the request is from Medusa (which is indicated by the `is_from_medusa` query parameter), the validation is skipped. + +Otherwise, you only allow updating the options if the number of options remains the same and each option has a `medusa_id` that matches an existing option in the previous value. + +Next, replace the `validate` function in the `variants` field with the following: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +export const Products: CollectionConfig = { + // other configurations... + fields: [ + // other fields... + { + name: "variants", + // other configurations... + validate: (value: any, { req, previousValue }) => { + if (req.query.is_from_medusa) { + return true // Skip validation if the request is from Medusa + } + + if (!Array.isArray(value)) { + return "Variants must be an array" + } + + const changedVariants = value.length !== previousValue?.length || value.some((variant: any) => { + return !variant.medusa_id || !previousValue?.some( + (prevVariant: any) => prevVariant.medusa_id === variant.medusa_id + ) + }) + + if (changedVariants) { + // Prevent update if the number of variants is changed + return "Variants cannot be changed in number" + } + + const changedOptionValues = value.some((variant: any) => { + if (!Array.isArray(variant.option_values)) { + return true // Invalid structure + } + + const previousVariant = previousValue?.find( + (v: any) => v.medusa_id === variant.medusa_id + ) as Record | undefined + + return variant.option_values.length !== previousVariant?.option_values.length || + variant.option_values.some((optionValue: any) => { + return !optionValue.medusa_id || !previousVariant?.option_values.some( + (prevOptionValue: any) => prevOptionValue.medusa_id === optionValue.medusa_id + ) + }) + }) + + return !changedOptionValues || "Option values cannot be changed in number" + }, + }, + ], +} +``` + +If the request is from Medusa, the validation is skipped. + +Otherwise, the function validates that: + +- The number of variants is the same as the previous value. +- Each variant has a `medusa_id` that matches an existing variant in the previous value. +- The number of option values for each variant is the same as the previous value. +- Each option value has a `medusa_id` that matches an existing option value in the previous value. + +If any of these validations fail, an error message is returned, preventing the update. + +### h. Add Hooks to Sync Product Data + +Next, you'll add a `beforeChange` hook to the `Products` collection that will normalize incoming `description` data to [rich-text format](https://payloadcms.com/docs/rich-text/overview). + +In `src/collections/Products.ts`, add the following import statement at the top of the file: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +import { convertLexicalToMarkdown, convertMarkdownToLexical, editorConfigFactory } from "@payloadcms/richtext-lexical" +``` + +Then, in the `Products` collection, add a `beforeChange` property to the `hooks` configuration: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +export const Products: CollectionConfig = { + // other configurations... + hooks: { + beforeChange: [ + async ({ data, req }) => { + if (typeof data.description === "string") { + data.description = convertMarkdownToLexical({ + editorConfig: await editorConfigFactory.default({ + config: req.payload.config, + }), + markdown: data.description, + }) + } + + return data + }, + ], + }, +} +``` + +This hook checks if the `description` field is a string and converts it to rich-text format. This ensures that a description coming from Medusa is properly formatted when stored in Payload. + +## i. Generate Payload Imports Map + +Before running the Payload admin, you need to generate the imports map that Payload uses to resolve the collections and other configurations. + +Run the following command in the Next.js Starter Storefront directory: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npx payload generate:importmap +``` + +This command generates the `src/app/(payload)/admin/importMap.js` file that Payload needs. + +## j. Run the Payload Admin + +You can now run the Payload admin in the Next.js Starter Storefront and create an admin user. + +To start the Next.js Starter Storefront, run the following command in the Next.js Starter Storefront directory: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +Then, open the Payload admin in your browser at `http://localhost:8000/admin`. The first time you access it, Payload will create a database at the connection URL you provided in the `.env.local` file. + +Then, you'll see a form to create a new admin user. Enter the user's credentials and submit the form. + +Once you're logged in, you can see the `Products`, `Users`, and `Media` collections in the Payload admin. + +![Payload Admin Dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1754477731/Medusa%20Resources/CleanShot_2025-08-06_at_13.55.06_2x_bhdkkd.png) + +*** + +## Step 3: Integrate Payload with Medusa + +Now that Payload is set up in the Next.js Starter Storefront, you'll create a Payload [Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to integrate it with Medusa. + +A module is a reusable package that provides functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure. + +### a. Create Module Directory + +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/payload`. + +### b. Create Types for the Module + +Next, you'll create a types file that will hold the types for the module's options and service methods. + +Create the file `src/modules/payload/types.ts` with the following content: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadModuleOptions { + serverUrl: string; + apiKey: string; + userCollection?: string; +} +``` + +For now, the file only contains the `PayloadModuleOptions` interface, which defines the options that the module will receive. It includes: + +- `serverUrl`: The URL of the Payload server. +- `apiKey`: The API key for authenticating with the Payload server. +- `userCollection`: The name of the user collection in Payload. This is optional and defaults to `users`. It's useful for the authentication header when sending requests to the Payload API. + +### c. Create Service + +A module has a service that contains its logic. So, the Payload Module's service will contain the logic to create, update, retrieve, and delete data in Payload. + +Create the file `src/modules/payload/service.ts` with the following content: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadModuleOptions, +} from "./types" +import { MedusaError } from "@medusajs/framework/utils" + +type InjectedDependencies = { + // inject any dependencies you need here +}; + +export default class PayloadModuleService { + private baseUrl: string + private headers: Record + private defaultOptions: Record = { + is_from_medusa: true, + } + + constructor( + container: InjectedDependencies, + options: PayloadModuleOptions + ) { + this.validateOptions(options) + this.baseUrl = `${options.serverUrl}/api` + + this.headers = { + "Content-Type": "application/json", + "Authorization": `${ + options.userCollection || "users" + } API-Key ${options.apiKey}`, + } + } + + validateOptions(options: Record): void | never { + if (!options.serverUrl) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Payload server URL is required" + ) + } + + if (!options.apiKey) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Payload API key is required" + ) + } + } +} +``` + +The constructor of a module's service receives the following parameters: + +1. The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that allows you to resolve module and Framework resources. You don't need to resolve any resources in this module, so you can leave it empty. +2. The module options, which you'll [pass to the module when you register it later](#e-add-module-to-medusas-configurations) in the Medusa application. + +In the constructor, you validate the module options and set up the Payload base URL and headers that are necessary to send requests to Payload. + +### c. Add Methods to the Service + +Next, you'll add methods to the service that allow you to create, update, retrieve, and delete products in Payload. + +#### makeRequest Method + +The `makeRequest` private method is a utility function that makes HTTP requests to the Payload API. You'll use this method in other public methods that perform operations in Payload. + +Add the `makeRequest` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}` + + try { + const response = await fetch(url, { + ...options, + headers: { + ...this.headers, + ...options.headers, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Payload API error: ${response.status} ${response.statusText}. ${ + errorData.message || "" + }` + ) + } + + return await response.json() + } catch (error) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Failed to communicate with Payload: ${JSON.stringify(error)}` + ) + } + } +} +``` + +The `makeRequest` method receives the endpoint to call and the options for the request. It constructs the full URL, makes the request, and returns the response data as JSON. + +If the request fails, it throws a `MedusaError` with the error message. + +#### create Method + +The `create` method will allow you to create an entry in a Payload collection, such as `Products`. + +Before you create the method, you'll need to add necessary types for its parameters and return value. + +In `src/modules/payload/types.ts`, add the following types: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadCollectionItem { + id: string; + createdAt: string; + updatedAt: string; + medusa_id: string; + [key: string]: any; +} + +export interface PayloadUpsertData { + [key: string]: any; +} + +export interface PayloadQueryOptions { + depth?: number; + locale?: string; + fallbackLocale?: string; + select?: string; + populate?: string; + limit?: number; + page?: number; + sort?: string; + where?: Record; +} + +export interface PayloadItemResult { + doc: T; + message: string; +} +``` + +You define the following types: + +- `PayloadCollectionItem`: an item in a Payload collection. +- `PayloadUpsertData`: the data required to create or update an item in a Payload collection. +- `PayloadQueryOptions`: the options for querying items in a Payload collection, which you can learn more about in the [Payload documentation](https://payloadcms.com/docs/queries/overview). +- `PayloadItemResult`: the result of a querying or performing an operation on a Payload item, which includes the item and a message. + +Next, add the following import statements at the top of the `src/modules/payload/service.ts` file: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadCollectionItem, + PayloadUpsertData, + PayloadQueryOptions, + PayloadItemResult, +} from "./types" +import qs from "qs" +``` + +You import the types you just defined and the `qs` library, which you'll use to stringify query options. + +Then, add the `create` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async create( + collection: string, + data: PayloadUpsertData, + options: PayloadQueryOptions = {} + ): Promise> { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}/${stringifiedQuery}` + + const result = await this.makeRequest>(endpoint, { + method: "POST", + body: JSON.stringify(data), + }) + return result + } +} +``` + +The `create` method receives the following parameters: + +- `collection`: the slug of the collection in Payload where you want to create an item. For example, `products`. +- `data`: the data for the new item you want to create. +- `options`: optional query options for the request. + +In the method, you use the `makeRequest` method to send a `POST` request to Payload, passing it the endpoint and request body data. + +Finally, you return the result of the request that contains the created item and a message. + +#### update Method + +Next, you'll add the `update` method that allows you to update an existing item in a Payload collection. + +Add the `update` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async update( + collection: string, + data: PayloadUpsertData, + options: PayloadQueryOptions = {} + ): Promise> { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}/${stringifiedQuery}` + + const result = await this.makeRequest>(endpoint, { + method: "PATCH", + body: JSON.stringify(data), + }) + + return result + } +} +``` + +Similar to the `create` method, the `update` method receives the collection slug, the data to update, and optional query options. + +In the method, you use the `makeRequest` method to send a `PATCH` request to Payload, passing it the endpoint and request body data. + +Finally, you return the result of the request that contains the updated item and a message. + +#### delete Method + +Next, you'll add the `delete` method that allows you to delete an item from a Payload collection. + +First, add the following type to `src/modules/payload/types.ts`: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadApiResponse { + data?: T; + errors?: Array<{ + message: string; + field?: string; + }>; + message?: string; +} +``` + +This represents a generic response from Payload, which can include data, errors, and a message. + +Then, add the following import statement at the top of the `src/modules/payload/service.ts` file: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadApiResponse, +} from "./types" +``` + +After that, add the `delete` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async delete( + collection: string, + options: PayloadQueryOptions = {} + ): Promise { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}/${stringifiedQuery}` + + const result = await this.makeRequest(endpoint, { + method: "DELETE", + }) + + return result + } +} +``` + +The `delete` method receives as parameters the collection slug and optional query options. + +In the method, you use the `makeRequest` method to send a `DELETE` request to Payload, passing it the endpoint. + +Finally, you return the result of the request that contains any data, errors, or a message. + +#### find Method + +The last method you'll add for now is the `find` method, which allows you to retrieve items from a Payload collection. + +First, add the following type to `src/modules/payload/types.ts`: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadBulkResult { + docs: T[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + pagingCounter: number; +} +``` + +This type represents the result of a bulk query to a Payload collection, which includes an array of documents and pagination information. + +Then, add the following import statement at the top of the `src/modules/payload/service.ts` file: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadBulkResult, +} from "./types" +``` + +After that, add the `find` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + async find( + collection: string, + options: PayloadQueryOptions = {} + ): Promise> { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}${stringifiedQuery}` + + const result = await this.makeRequest< + PayloadBulkResult + >(endpoint) + + return result + } +} +``` + +The `find` method receives the collection slug and optional query options. + +In the method, you use the `makeRequest` method to send a `GET` request to Payload, passing it the endpoint with the query options. + +Finally, you return the result of the request that contains an array of documents and pagination information. + +### d. Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders. + +To create the module's definition, create the file `src/modules/payload/index.ts` with the following content: + +```ts title="src/modules/payload/index.ts" badgeLabel="Medusa application" badgeColor="green" +import { Module } from "@medusajs/framework/utils" +import PayloadModuleService from "./service" + +export const PAYLOAD_MODULE = "payload" + +export default Module(PAYLOAD_MODULE, { + service: PayloadModuleService, +}) +``` + +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `payload`. +2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts. + +Aside from the module definition, you export the module's name as `PAYLOAD_MODULE` so you can reference it later. + +### e. Add Module to Medusa's Configurations + +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: + +```ts title="medusa-config.ts" badgeLabel="Medusa application" badgeColor="green" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/payload", + options: { + serverUrl: process.env.PAYLOAD_SERVER_URL || "http://localhost:8000", + apiKey: process.env.PAYLOAD_API_KEY, + userCollection: process.env.PAYLOAD_USER_COLLECTION || "users", + }, + }, + ], +}) +``` + +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name. + +You also pass an `options` property with the module's options. You'll set the values of these options next. + +### f. Set Environment Variables + +To use the Payload Module, you need to set the module options in the environment variables of your Medusa application. + +One of these options is the API key of a Payload admin user. To get the API key: + +1. Start the Next.js Starter Storefront with the following command: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +2. Open `localhost:8000/admin` in your browser and log in with the admin user you created earlier. +3. Click on the "Users" collection in the sidebar. +4. Choose your admin user from the list. +5. Click on the "Enable API key" checkbox and copy the API key that appears. +6. Click the "Save" button to save the changes. + +![The user form with the API key enabled](https://res.cloudinary.com/dza7lstvk/image/upload/v1754479684/Medusa%20Resources/CleanShot_2025-08-06_at_14.27.25_2x_gasihl.png) + +Next, add the following environment variables to your Medusa application's `.env` file: + +```shell title=".env" badgeLabel="Medusa application" badgeColor="green" +PAYLOAD_SERVER_URL=http://localhost:8000 +PAYLOAD_API_KEY=your_api_key_here +PAYLOAD_USER_COLLECTION=users +``` + +Make sure to replace `your_api_key_here` with the API key you copied from the Payload admin. + +The Payload Module is now ready for use. You'll add customizations next to sync product data between Medusa and Payload. + +*** + +## Step 4: Create Virtual Read-Only Link to Products + +Medusa's [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) feature allows you to virtually link data models from external services to modules in your Medusa application. Then, when you retrieve data from Medusa, you can also retrieve the linked data from the third-party service automatically. + +In this step, you'll define a virtual read-only link between the `Products` collection in Payload and the `Product` model in Medusa. Later, you'll be able to retrieve products from Payload while retrieving products in Medusa. + +### a. Define the Link + +To define a virtual read-only link, create the file `src/links/product-payload.ts` with the following content: + +```ts title="src/links/product-payload.ts" badgeLabel="Medusa application" badgeColor="green" +import { defineLink } from "@medusajs/framework/utils" +import ProductModule from "@medusajs/medusa/product" +import { PAYLOAD_MODULE } from "../modules/payload" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + }, + { + linkable: { + serviceName: PAYLOAD_MODULE, + alias: "payload_product", + primaryKey: "product_id", + }, + }, + { + readOnly: true, + } +) +``` + +The `defineLink` function accepts three parameters: + +- An object of the first data model that is part of the link. In this case, it's the `Product` model from Medusa's Product Module. +- An object of the second data model that is part of the link. In this case, it's the `Products` collection from the Payload Module. You set the following properties: + - `serviceName`: the name of the Payload Module, which is `payload`. + - `alias`: an alias for the linked data model, which is `payload_product`. You'll use this alias to reference the linked data model in queries. + - `primaryKey`: the primary key of the linked data model, which is `product_id`. Medusa will look for this field in the retrieved `Products` from payload to match it with the `id` field of the `Product` model. +- An object with the `readOnly` property set to `true`, indicating that this link is read-only. This means you can only retrieve the linked data, but you don't manage the link in the database. + +### b. Add list Method to the Payload Module Service + +When you retrieve products from Medusa with their `payload_product` link, Medusa will call the `list` method of the Payload Module's service to retrieve the linked products from Payload. + +So, in `src/modules/payload/service.ts`, add a `list` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async list( + filter: { + product_id: string | string[] + } + ) { + const collection = filter.product_id ? "products" : "unknown" + const ids = Array.isArray(filter.product_id) ? filter.product_id : [filter.product_id] + const result = await this.find( + collection, + { + where: { + medusa_id: { + in: ids.join(","), + }, + }, + depth: 2, + } + ) + + return result.docs.map((doc) => ({ + ...doc, + product_id: doc.medusa_id, + })) + } +} +``` + +The `list` method receives a `filter` object with an `product_id` property, which is the Medusa product ID(s) to retrieve their corresponding data from Payload. + +In the method, you call the `find` method of the Payload Module's service to retrieve products from the `products` collection in Payload. You pass a `where` query parameter to filter products by their `medusa_id` field. + +Finally, you return an array of the payload products. You set the `product_id` field to the value of the `medusa_id` field, which is used to match the linked data in Medusa. + +You can now retrieve products from Payload while retrieving products in Medusa. You'll learn how to do this in the upcoming steps. + +The `list` method is implemented to be re-usable with different collections and data models. For example, if you add a `Categories` collection in Payload, you can use the same `list` method to retrieve categories by their `medusa_id` field. In that case, the `filter` object would have a `category_id` property instead of `product_id`, and you can set the `collection` variable to `"categories"`. + +*** + +## Step 5: Create Payload Product Workflow + +In this step, you'll create the functionality to create a Medusa product in Payload. You'll later execute that functionality either when triggered by an admin user, or automatically when a product is created in Medusa. + +You create custom commerce features in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features. + +Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more. + +The workflow to create a Payload product will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product data from Medusa +- [createPayloadItemsStep](#createPayloadItemsStep): Create the product in Payload +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Store the Payload product ID in Medusa + +You only need to create the `createPayloadItemsStep`, as the other two steps are already available in Medusa. + +### createPayloadItemsStep + +The `createPayloadItemsStep` will create an item in a Payload collection, such as `Products`. + +To create the step, create the file `src/workflows/steps/create-payload-items.ts` with the following content: + +```ts title="src/workflows/steps/create-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" highlights={createPayloadItemsStepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PayloadUpsertData } from "../../modules/payload/types" +import { PAYLOAD_MODULE } from "../../modules/payload" + +type StepInput = { + collection: string + items: PayloadUpsertData[] +} + +export const createPayloadItemsStep = createStep( + "create-payload-items", + async ({ items, collection }: StepInput, { container }) => { + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + const createdItems = await Promise.all( + items.map(async (item) => await payloadModuleService.create( + collection, + item + )) + ) + + return new StepResponse({ + items: createdItems.map((item) => item.doc), + }, { + ids: createdItems.map((item) => item.doc.id), + collection, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + const { ids, collection } = data + + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + await payloadModuleService.delete( + collection, + { + where: { + id: { + in: ids.join(","), + }, + }, + } + ) + } +) +``` + +You create a step with the `createStep` function. It accepts three parameters: + +1. The step's unique name. +2. An async function that receives two parameters: + - The step's input, which is an object holding the collection slug and an array of items to create in Payload. + - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. +3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution. + +In the step function, you resolve the Payload Module's service from the container. Then, you use its `create` method to create the items in Payload. + +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: + +1. The step's output, which is an object that contains the created items. +2. Data to pass to the step's compensation function. + +In the compensation function, you again resolve the Payload Module's service from the Medusa container, then delete the created items from Payload. + +### Create Payload Product Workflow + +You can now create the workflow that creates products in Payload. + +To create the workflow, create the file `src/workflows/create-payload-products.ts` with the following content: + +```ts title="src/workflows/create-payload-products.ts" badgeLabel="Medusa application" badgeColor="green" highlights={createPayloadProductsWorkflowHighlights} +import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { createPayloadItemsStep } from "./steps/create-payload-items" +import { updateProductsWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + product_ids: string[] +} + +export const createPayloadProductsWorkflow = createWorkflow( + "create-payload-products", + (input: WorkflowInput) => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "id", + "title", + "handle", + "subtitle", + "description", + "created_at", + "updated_at", + "options.*", + "variants.*", + "variants.options.*", + "thumbnail", + "images.*", + ], + filters: { + id: input.product_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const createData = transform({ + products, + }, (data) => { + return { + collection: "products", + items: data.products.map((product) => ({ + medusa_id: product.id, + createdAt: product.created_at as string, + updatedAt: product.updated_at as string, + title: product.title, + handle: product.handle, + subtitle: product.subtitle, + description: product.description || "", + options: product.options.map((option) => ({ + title: option.title, + medusa_id: option.id, + })), + variants: product.variants.map((variant) => ({ + title: variant.title, + medusa_id: variant.id, + option_values: variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: option.value, + })), + })), + })), + } + }) + + const { items } = createPayloadItemsStep( + createData + ) + + const updateData = transform({ + items, + }, (data) => { + return data.items.map((item) => ({ + id: item.medusa_id, + metadata: { + payload_id: item.id, + }, + })) + }) + + updateProductsWorkflow.runAsStep({ + input: { + products: updateData, + }, + }) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter. + +It accepts a second parameter: a constructor function that holds the workflow's implementation. The function accepts an input object holding the IDs of the products to create in Payload. + +In the workflow, you: + +1. Retrieve the products from Medusa using the `useQueryGraphStep`. + - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. +2. Prepare the data to create the products in Payload. + - To manipulate data in a workflow, you need to use the `transform` function. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation. +3. Create the products in Payload using the `createPayloadItemsStep` you created earlier. +4. Prepare the data to update the products in Medusa with the Payload product IDs. + - You store the payload ID in the `metadata` field of the Medusa product. +5. Update the products in Medusa using the `updateProductsWorkflow`. + +A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor. + +You'll use this workflow in the next steps to create Medusa products in Payload. + +*** + +## Step 6: Trigger Product Creation in Payload + +In this step, you'll allow Medusa Admin users to trigger the creation of Medusa products in Payload. To implement this, you'll create: + +- An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that emits a `products.sync-payload` event. +- A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `products.sync-payload` event and executes the `createPayloadProductsWorkflow`. +- A [setting page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) in the Medusa Admin that allows admin users to trigger the product creation in Payload. + +### a. Trigger Product Sync API Route + +An API route is a REST endpoint that exposes functionalities to clients, such as storefronts and the Medusa Admin. + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`. + +Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more about them. + +Create the file `src/api/admin/payload/sync/[collection]/route.ts` with the following content: + +```ts title="src/api/admin/payload/sync/[collection]/route.ts" badgeLabel="Medusa application" badgeColor="green" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { collection } = req.params + const eventModuleService = req.scope.resolve("event_bus") + + await eventModuleService.emit({ + name: `${collection}.sync-payload`, + data: {}, + }) + + return res.status(200).json({ + message: `Syncing ${collection} with Payload`, + }) +} +``` + +Since you export a `POST` route handler function, you're exposing a `POST` API route at `/admin/payload/sync/[collection]`, where `[collection]` is a path parameter that represents the collection slug in Payload. + +In the function, you resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service and emit a `{collection}.sync-payload` event, where `{collection}` is the collection slug passed in the request. + +Finally, you return a success response with a message indicating that the collection is being synced with Payload. + +### b. Create Subscriber for the Event + +Next, you'll create a subscriber that listens to the `products.sync-payload` event and executes the `createPayloadProductsWorkflow`. + +A subscriber is an asynchronous function that is executed whenever its associated event is emitted. + +Refer to the [Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) to learn more about subscribers. + +To create a subscriber, create the file `src/subscribers/products-sync-payload.ts` with the following content: + +```ts title="src/subscribers/products-sync-payload.ts" badgeLabel="Medusa application" badgeColor="green" highlights={productsSyncPayloadSubscriberHighlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductsWorkflow } from "../workflows/create-payload-products" + +export default async function productSyncPayloadHandler({ + container, +}: SubscriberArgs) { + const query = container.resolve("query") + + const limit = 1000 + let offset = 0 + let count = 0 + + do { + const { + data: products, + metadata: { count: totalCount } = {}, + } = await query.graph({ + entity: "product", + fields: [ + "id", + "metadata", + ], + pagination: { + take: limit, + skip: offset, + }, + }) + + count = totalCount || 0 + offset += limit + const filteredProducts = products.filter((product) => !product.metadata?.payload_id) + + if (filteredProducts.length === 0) { + break + } + + await createPayloadProductsWorkflow(container) + .run({ + input: { + product_ids: filteredProducts.map((product) => product.id), + }, + }) + + } while (count > offset + limit) +} + +export const config: SubscriberConfig = { + event: "products.sync-payload", +} +``` + +A subscriber file must export: + +- An asynchronous function, which is the subscriber function that is executed when the event is emitted. +- A configuration object that defines the event the subscriber listens to. + +In the subscriber, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve all products from Medusa. + +Then, you filter the products to only include those that don't have a payload product ID set in `product.metadata.payload_id`, and you execute the `createPayloadProductsWorkflow` with the filtered products' IDs. + +Whenever the `products.sync-payload` event is emitted, the subscriber will be executed, which will create the products in Payload. + +### c. Create Setting Page in Medusa Admin + +Next, you'll create a setting page in the Medusa Admin that allows admin users to trigger syncing products with Payload. + +#### Initialize JS SDK + +To send requests from your Medusa Admin customizations to the Medusa server, you need to initialize the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). + +Create the file `src/admin/lib/sdk.ts` with the following content: + +```ts title="src/admin/lib/sdk.ts" badgeLabel="Medusa application" badgeColor="green" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about initializing the SDK. + +#### Create the Setting Page + +A setting page is a [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) that adds a custom page to the Medusa Admin under the Settings section. The UI route is a React component that renders the page's content. + +Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation to learn more. + +To create the setting page, create the file `src/admin/routes/settings/payload/page.tsx` with the following content: + +```tsx title="src/admin/routes/settings/payload/page.tsx" badgeLabel="Medusa application" badgeColor="green" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading, toast } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../../../lib/sdk" + +const PayloadSettingsPage = () => { + const { + mutateAsync: syncProductsToPayload, + isPending: isSyncingProductsToPayload, + } = useMutation({ + mutationFn: (collection: string) => + sdk.client.fetch(`/admin/payload/sync/${collection}`, { + method: "POST", + }), + onSuccess: () => toast.success(`Triggered syncing collection data with Payload`), + }) + + return ( + +
+ Payload Settings +
+
+

+ This page allows you to trigger syncing your Medusa data with Payload. It + will only create items not in Payload. +

+ +
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Payload", +}) + +export default PayloadSettingsPage +``` + +A settings page file must export: + +1. A React component that renders the page. This is the file's default export. +2. A configuration object created with the `defineRouteConfig` function. It accepts an object with properties that define the page's configuration, such as its sidebar label. + +In the page's component, you define a mutation function using Tanstack Query and the JS SDK. This function will send a `POST` request to the API route you created earlier to trigger syncing products with Payload. + +Then, you render a button that, when clicked, calls the mutation function to trigger the syncing process. + +### d. Test Product Syncing + +You can now test syncing products from Medusa to Payload. To do that: + +1. Start your Medusa application with the following command: + +```bash npm2yarn badgeLabel="Medusa application" badgeColor="green" +npm run dev +``` + +2. Run the Next.js Starter Storefront with the command: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +3. Open the Medusa Admin at `localhost:9000/app` and log in with your admin user. +4. Go to Settings -> Payload. +5. On the setting page, click the "Sync Products to Payload" button. + +![The Payload Settings page with the Sync Products button](https://res.cloudinary.com/dza7lstvk/image/upload/v1754486648/Medusa%20Resources/CleanShot_2025-08-06_at_16.23.45_2x_ubug2y.png) + +You'll see a success message indicating that the products are being synced with Payload. You can also confirm that the event was triggered by checking the Medusa server logs for the following message: + +```bash +info: Processing products.sync-payload which has 1 subscribers +``` + +To check that the products were created in Payload, open the Payload admin at `localhost:8000/admin` and go to "Products" from the sidebar. You should see your Medusa products listed there. + +![The Products collection in Payload with Medusa products](https://res.cloudinary.com/dza7lstvk/image/upload/v1754486747/Medusa%20Resources/CleanShot_2025-08-06_at_16.25.31_2x_h874oa.png) + +If you click on a product, you can edit its details, such as its title or description. + +*** + +## Step 7: Automatically Create Product in Payload + +In this step, you'll handle the `product.created` event to automatically create a product in Payload whenever a product is created in Medusa. + +You already have the workflow to create a product in Payload, so you only need to create a subscriber that listens to the `product.created` event and executes the `createPayloadProductsWorkflow`. + +Create the file `src/subscribers/product-created.ts` with the following content: + +```ts title="src/subscribers/product-created.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductsWorkflow } from "../workflows/create-payload-products" + +export default async function productCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await createPayloadProductsWorkflow(container) + .run({ + input: { + product_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +This subscriber listens to the `product.created` event and executes the `createPayloadProductsWorkflow` with the created product's ID. + +### Test Automatic Product Creation + +To test out the automatic product creation in Payload, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, create a product in Medusa using the Medusa Admin. If you check the Products collection in the Payload admin, you should see the newly created product there as well. + +*** + +## Step 8: Customize Storefront to Display Payload Products + +Now that you've integrated Payload with Medusa, you can customize the Next.js Starter Storefront to display product content from Payload. By doing so, you can show product content and assets that are optimized for the storefront. + +In this step, you'll customize the Next.js Starter Storefront to view the product title, description, images, and option values from Payload. + +### a. Fetch Payload Data with Product Data + +When you fetch product data in the Next.js Starter Storefront from the Medusa server, you can also retrieve the linked product data from Payload. + +To do this, go to `src/lib/products.ts` in your Next.js Starter Storefront. You'll find a `listProducts` function that uses the JS SDK to fetch products from the Medusa server. + +Find the `sdk.client.fetch` call and add `*payload_product` to the `fields` query parameter: + +```ts title="src/lib/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["19"]]} +export const listProducts = async ({ + // ... +}: { + //... +}): Promise<{ + // ... +}> => { + // ... + return sdk.client + .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>( + `/store/products`, + { + method: "GET", + query: { + limit, + offset, + region_id: region?.id, + fields: + "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*payload_product", + ...queryParams, + }, + headers, + next, + cache: "force-cache", + } + ) + // ... +} +``` + +Passing this field is possible because you [defined the virtual read-only link](#step-4-create-virtual-read-only-link-to-products) between the `Product` model in Medusa and the `Products` collection in Payload. + +Medusa will now return the payload data of a product from Payload and include it in the `payload_product` field of the product object. + +### b. Define Payload Product Type + +Next, you'll define a TypeScript type that adds the `payload_product` property to Medusa's `StoreProduct` type. + +In `src/types/global.ts`, add the following imports at the top of the file: + +```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue" +import { StoreProduct } from "@medusajs/types" +// @ts-ignore +import type { SerializedEditorState } from "@payloadcms/richtext-lexical/lexical" +``` + +Then, add the following type definition at the end of the file: + +```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue" +export type StoreProductWithPayload = StoreProduct & { + payload_product?: { + medusa_id: string + title: string + handle: string + subtitle?: string + description?: SerializedEditorState + thumbnail?: { + id: string + url: string + } + images: { + id: string + image: { + id: string + url: string + } + }[] + options: { + medusa_id: string + title: string + }[] + variants: { + medusa_id: string + title: string + option_values: { + medusa_option_id: string + value: string + }[] + }[] + } +} +``` + +The `StoreProductWithPayload` type extends the `StoreProduct` type from Medusa and adds the `payload_product` property. This property contains the product data from Payload, including its title, description, images, options, and variants. + +### c. Display Payload Product Title and Description + +Next, you'll customize the product details page to display the product title and description from Payload. + +To do that, you need to customize the `ProductInfo` component in `src/modules/products/templates/product-info/index.tsx`. + +First, add the following import statement at the top of the file: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../../types/global" +// @ts-ignore +import { RichText } from "@payloadcms/richtext-lexical/react" +``` + +Then, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductInfoProps = { + product: StoreProductWithPayload +} +``` + +Next, find in the `ProductInfo` component's `return` statement where the product title is displayed and replace it with the following: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"]]} +return ( +
+
+ {/* ... */} + + {product?.payload_product?.title || product.title} + + {/* ... */} +
+
+) +``` + +Also, find where the product description is displayed and replace it with the following: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} +return ( +
+
+ {/* ... */} + {product?.payload_product?.description !== undefined && + + } + + {product?.payload_product?.description === undefined && ( + + {product.description} + + )} + {/* ... */} +
+
+) +``` + +If the product has a description in Payload, it will be displayed using Payload's `RichText` component, which renders the rich text content. Otherwise, it will display the product description from Medusa. + +### d. Display Payload Product Images + +Next, you'll display the product images from Payload in the product details page and in the product preview component that is shown in the product list. + +#### Add Image Utility Functions + +You'll first create utility functions useful for retrieving the images of a product. + +Create the file `src/lib/util/payload-images.ts` with the following content: + +```ts title="src/lib/util/payload-images.ts" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../types/global" + +export function getProductImages(product: StoreProductWithPayload) { + return product?.payload_product?.images?.map((image) => ({ + id: image.id, + url: formatPayloadImageUrl(image.image.url), + })) || product.images || [] +} + +export function formatPayloadImageUrl(url: string): string { + return url.replace(/^\/api\/media\/file/, "") +} +``` + +You define two functions: + +- `getProductImages`: This function accepts a product and returns either the images from Payload or the images from Medusa if the product doesn't have images in Payload. +- `formatPayloadImageUrl`: This function formats the image URL from Payload by removing the `/api/media/file` prefix, which is not needed for displaying the image in the storefront. + +#### Update ImageGallery Props + +Next, you'll update the type of the `ImageGallery` component's props to receive an array of objects rather than an array of Medusa images. This ensures the component can accept images from Payload. + +In `src/modules/products/components/image-gallery/index.tsx`, update the `ImageGalleryProps` type to the following: + +```tsx title="src/modules/products/components/image-gallery/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ImageGalleryProps = { + images: { + id: string + url: string + }[] +} +``` + +The `ImageGallery` component can now accept an array of image objects, each with an `id` and a `url`. + +#### Display Images in Product Details Page + +To display the product images in the product details page, add the following imports at the top of `src/modules/products/templates/index.tsx`: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../types/global" +import { getProductImages } from "../../../lib/util/payload-images" +``` + +Next, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductTemplateProps = { + product: StoreProductWithPayload + // ... +} +``` + +Then, add the following before the `ProductTemplate` component's `return` statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["7"]]} +const ProductTemplate: React.FC = ({ + product, + region, + countryCode, +}) => { + // ... + const productImages = getProductImages(product) + // ... +} +``` + +You retrieve the images to display using the `getProductImages` function you created earlier. + +Finally, update the `images` prop of the `ImageGallery` component in the `return` statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]} +return ( + <> + {/* ... */} + + {/* ... */} + +) +``` + +The images on the product's details page will now be the images from Payload if available, or the images from Medusa if not. + +#### Display Images in Product Preview + +To display the product images in the product preview component that is displayed in the product list, add the following imports at the top of `src/modules/products/components/product-preview/index.tsx`: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../../types/global" +import { formatPayloadImageUrl, getProductImages } from "../../../../lib/util/payload-images" +``` + +Then, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]} +export default async function ProductPreview({ + product, + // ... +}: { + product: StoreProductWithPayload + // ... +}) { + // ... +} +``` + +Next, add the following before the `return` statement: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["8"]]} +export default async function ProductPreview({ + // ... +}: { + // ... +}) { + // ... + + const productImages = getProductImages(product) + + // ... +} +``` + +You retrieve the images to display using the `getProductImages` function you created earlier. + +After that, update the `thumbnail` and `images` props of the `Thumbnail` component in the `return` statement: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"]]} +return ( + + {/* ... */} + + {/* ... */} + +) +``` + +The thumbnail shown in the product listing will now use the thumbnail from Payload if available, or the thumbnail from Medusa if not. + +You'll also display the product title from Payload in the product preview. Find the following lines in the `return` statement: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]} +return ( + + {/* ... */} + + {product.title} + + {/* ... */} + +) +``` + +And replace them with the following: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]} +return ( + + {/* ... */} + + {product.payload_product?.title || product.title} + + {/* ... */} + +) +``` + +The product title in the product preview will now be the title from Payload if available, or the title from Medusa if not. + +### e. Display Product Options and Values + +The last change you'll make is to display the title of product options and their values from Payload in the product details page. + +In `src/modules/products/components/product-actions/index.tsx`, add the following import at the top of the file: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../../types/global" +``` + +Then, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductActionsProps = { + product: StoreProductWithPayload + // ... +} +``` + +Next, find the `optionsAsKeymap` function and replace it with the following: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const optionsAsKeymap = ( + variantOptions: HttpTypes.StoreProductVariant["options"], + payloadData: StoreProductWithPayload["payload_product"] +) => { + const firstVariant = payloadData?.variants?.[0] + return variantOptions?.reduce((acc: Record, varopt: any) => { + acc[varopt.option_id] = firstVariant?.option_values.find( + (v) => v.medusa_option_id === varopt.id + )?.value || varopt.value + return acc + }, {}) +} +``` + +You update the function to receive a `payloadData` parameter, which is the product data from Payload. This allows you to retrieve the option values from Payload instead of Medusa. + +Then, in the `ProductActions` component, update all usages of the `optionsAsKeymap` function to pass the `product.payload_product` data: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]} +// update all usages of optionsAsKeymap +const variantOptions = optionsAsKeymap( + product.variants[0].options, + product.payload_product +) +``` + +Finally, in the `return` statement, find the loop over `product.options` and replace it with the following: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={productActionsComponentHighlights} +return ( + <> + {/* ... */} + {(product.options || []).map((option) => { + const payloadOption = product.payload_product?.options?.find( + (o) => o.medusa_id === option.id + ) + return ( +
+ +
+ ) + })} + {/* ... */} + +) +``` + +You change the `title` prop of the `OptionSelect` component to use the title from Payload if available, or the Medusa option title if not. + +Now, the product options and values will be displayed using the data from Payload, if available. + +### Test Storefront Customization + +To test out the storefront customization, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the storefront at `localhost:8000` and click on Menu -> Store. In the products listing page, you'll see thumbnails and titles of the products from Payload. + +![Product listing page in the storefront showing details from Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754491849/Medusa%20Resources/CleanShot_2025-08-06_at_17.50.34_2x_pj0hjs.png) + +If you click on a product, you'll see the product details page with the product title, description, images, and options from Payload. + +![Product details page in the storefront showing details from Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754491898/Medusa%20Resources/CleanShot_2025-08-06_at_17.51.28_2x_eokeyg.png) + +*** + +## Step 9: Handle Medusa Product Events + +In this step, you'll create subscribers and workflows to handle the following Medusa product events: + +- [product.deleted](#a-handle-product-deletions): Delete the product in Payload when a product is deleted in Medusa. +- [product-variant.created](#b-handle-product-variant-creation): Add a product variant to a product in Payload when a product variant is created in Medusa. +- [product-variant.updated](#c-handle-product-variant-updates): Update a product variant's option values in Payload when a product variant is updated in Medusa. +- [product-variant.deleted](#d-handle-product-variant-deletions): Remove a product's variant in Payload when a product variant is deleted in Medusa. +- [product-option.created](#e-handle-product-option-creation): Add a product option to a product in Payload when a product option is created in Medusa. +- [product-option.deleted](#f-handle-product-option-deletions): Remove a product's option in Payload when a product option is deleted in Medusa. + +### a. Handle Product Deletions + +To handle the `product.deleted` event, you'll create a workflow that deletes the product from Payload, then create a subscriber that executes the workflow when the event is emitted. + +The workflow will have the following steps: + +- [deletePayloadItemsStep](#deletePayloadItemsStep): Delete the product from Payload + +#### deletePayloadItemsStep + +First, you need to create the `deletePayloadItemsStep` that allows you to delete items from a Payload collection. + +Create the file `src/workflows/steps/delete-payload-items.ts` with the following content: + +```ts title="src/workflows/steps/delete-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PAYLOAD_MODULE } from "../../modules/payload" + +type StepInput = { + collection: string; + where: Record; +} + +export const deletePayloadItemsStep = createStep( + "delete-payload-items", + async ({ where, collection }: StepInput, { container }) => { + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + const prevData = await payloadModuleService.find(collection, { + where, + }) + + await payloadModuleService.delete(collection, { + where, + }) + + return new StepResponse({}, { + prevData, + collection, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + const { prevData, collection } = data + + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + for (const item of prevData.docs) { + await payloadModuleService.create( + collection, + item + ) + } + } +) +``` + +This step accepts a collection slug and a `where` condition to specify which items to delete from Payload. + +In the step, you first retrieve the existing items that match the `where` condition using the `find` method in the Payload Module's service. You pass these items to the compensation function so that you can restore them if an error occurs in the workflow. + +Then, you delete the items using the `delete` method of the Payload Module's service. + +#### Delete Payload Products Workflow + +Next, to create the workflow that deletes products from Payload, create the file `src/workflows/delete-payload-products.ts` with the following content: + +```ts title="src/workflows/delete-payload-products.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { deletePayloadItemsStep } from "./steps/delete-payload-items" + +type WorkflowInput = { + product_ids: string[] +} + +export const deletePayloadProductsWorkflow = createWorkflow( + "delete-payload-products", + ({ product_ids }: WorkflowInput) => { + const deleteProductsData = transform({ + product_ids, + }, (data) => { + return { + collection: "products", + where: { + medusa_id: { + in: data.product_ids.join(","), + }, + }, + } + }) + + deletePayloadItemsStep(deleteProductsData) + + return new WorkflowResponse(void 0) + } +) +``` + +This workflow receives the IDs of the products to delete from Payload. + +In the workflow, you prepare the data to delete from Payload using the `transform` function, then call the `deletePayloadItemsStep` to delete the products from Payload where the `medusa_id` matches one of the provided product IDs. + +#### Product Deleted Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product.deleted` event is emitted. + +Create the file `src/subscribers/product-deleted.ts` with the following content: + +```ts title="src/subscribers/product-deleted.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deletePayloadProductsWorkflow } from "../workflows/delete-payload-products" + +export default async function productDeletedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await deletePayloadProductsWorkflow(container) + .run({ + input: { + product_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.deleted", +} +``` + +This subscriber listens to the `product.deleted` event and executes the `deletePayloadProductsWorkflow` with the deleted product's ID. + +#### Test Product Deletion Handling + +To test the product deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and go to the products list. Delete a product that exists in Payload. + +If you check the Products collection in Payload, you should see that the product has been removed from there as well. + +### b. Handle Product Variant Creation + +To handle the `product-variant.created` event, you'll create a workflow that adds the new variant to the corresponding product in Payload. + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product variant from Medusa + +#### Create Payload Product Variant Workflow + +Create the file `src/workflows/create-payload-product-variant.ts` with the following content: + +```ts title="src/workflows/create-payload-product-variant.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types" +import { updatePayloadItemsStep } from "./steps/update-payload-items" + +type WorkflowInput = { + variant_ids: string[]; +} + +export const createPayloadProductVariantWorkflow = createWorkflow( + "create-payload-product-variant", + ({ variant_ids }: WorkflowInput) => { + const { data: productVariants } = useQueryGraphStep({ + entity: "product_variant", + fields: [ + "id", + "title", + "options.*", + "options.option.*", + "product.payload_product.*", + ], + filters: { + id: variant_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const updateData = transform({ + productVariants, + }, (data) => { + const items: Record = {} + + data.productVariants.forEach((variant) => { + // @ts-expect-error + const payloadProduct = variant.product?.payload_product as PayloadCollectionItem + if (!payloadProduct) {return} + if (!items[payloadProduct.id]) { + items[payloadProduct.id] = { + variants: payloadProduct.variants || [], + } + } + + items[payloadProduct.id].variants.push({ + title: variant.title, + medusa_id: variant.id, + option_values: variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: option.value, + })), + }) + }) + + return { + collection: "products", + items: Object.keys(items).map((id) => ({ + id, + ...items[id], + })), + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product variants to add to Payload. + +In the workflow, you: + +1. Retrieve the product variant details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload. +2. Prepare the data to update the product in Payload by adding the new variant to the existing variants array. +3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Variant Created Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-variant.created` event is emitted. + +Create the file `src/subscribers/variant-created.ts` with the following content: + +```ts title="src/subscribers/variant-created.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductVariantWorkflow } from "../workflows/create-payload-product-variant" + +export default async function productVariantCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await createPayloadProductVariantWorkflow(container) + .run({ + input: { + variant_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-variant.created", +} +``` + +This subscriber listens to the `product-variant.created` event and executes the `createPayloadProductVariantWorkflow` with the created variant's ID. + +#### Test Product Variant Creation Handling + +To test the product variant creation handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Add a new variant to the product and save the changes. + +If you check the product in Payload, you should see that the new variant has been added to the product's variants array. + +### c. Handle Product Variant Updates + +To handle the `product-variant.updated` event, you'll create a workflow that updates the variant in the corresponding product in Payload. + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product variant from Medusa + +#### Update Payload Product Variants Workflow + +Since you already have the necessary steps, you only need to create the workflow that uses these steps. + +Create the file `src/workflows/update-payload-product-variants.ts` with the following content: + +```ts title="src/workflows/update-payload-product-variants.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types" +import { updatePayloadItemsStep } from "./steps/update-payload-items" + +type WorkflowInput = { + variant_ids: string[]; +} + +export const updatePayloadProductVariantsWorkflow = createWorkflow( + "update-payload-product-variants", + ({ variant_ids }: WorkflowInput) => { + const { data: productVariants } = useQueryGraphStep({ + entity: "product_variant", + fields: [ + "id", + "title", + "options.*", + "options.option.*", + "product.payload_product.*", + ], + filters: { + id: variant_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const updateData = transform({ + productVariants, + }, (data) => { + const items: Record = {} + + data.productVariants.forEach((variant) => { + // @ts-expect-error + const payloadProduct = variant.product?.payload_product as PayloadCollectionItem + if (!payloadProduct) {return} + + if (!items[payloadProduct.id]) { + items[payloadProduct.id] = { + variants: payloadProduct.variants || [], + } + } + + // Find and update the existing variant in the payload product + const existingVariantIndex = items[payloadProduct.id].variants.findIndex( + (v: any) => v.medusa_id === variant.id + ) + + if (existingVariantIndex >= 0) { + // check if option values need to be updated + const existingVariant = items[payloadProduct.id].variants[existingVariantIndex] + const updatedOptionValues = variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: existingVariant.option_values.find((ov: any) => ov.medusa_id === option.id)?.value || + option.value, + })) + + items[payloadProduct.id].variants[existingVariantIndex] = { + ...existingVariant, + option_values: updatedOptionValues, + } + } else { + // Add the new variant to the payload product + items[payloadProduct.id].variants.push({ + title: variant.title, + medusa_id: variant.id, + option_values: variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: option.value, + })), + }) + } + }) + + return { + collection: "products", + items: Object.keys(items).map((id) => ({ + id, + ...items[id], + })), + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product variants to update in Payload. + +In the workflow, you: + +1. Retrieve the product variant details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload. +2. Prepare the data to update the product in Payload by finding and updating the existing variant in the variants array. You only update the variant's option values, in case a new one is added. +3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Variant Updated Subscriber + +Finally, you'll create the subscriber that executes the workflow. + +Create the file `src/subscribers/variant-updated.ts` with the following content: + +```ts title="src/subscribers/variant-updated.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { updatePayloadProductVariantsWorkflow } from "../workflows/update-payload-product-variants" + +export default async function productVariantUpdatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await updatePayloadProductVariantsWorkflow(container) + .run({ + input: { + variant_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-variant.updated", +} +``` + +This subscriber listens to the `product-variant.updated` event and executes the `updatePayloadProductVariantsWorkflow` with the updated variant's ID. + +#### Test Product Variant Update Handling + +To test the product variant update handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Edit an existing variant's title and save the changes. + +If you check the product in Payload, you should see that the variant's option values have been updated in the product's variants array. + +### d. Handle Product Variant Deletions + +To handle the `product-variant.deleted` event, you'll create a workflow that removes the variant from the corresponding product in Payload. + +The workflow will have the following steps: + +- [retrievePayloadItemsStep](#retrievePayloadItemsStep): Retrieve the products containing the variant from Payload + +#### retrievePayloadItemsStep + +Since the `deletePayloadProductVariantsWorkflow` is executed after a product variant is deleted, you can't retrieve the product variant data from Medusa. + +Instead, you'll create a step that retrieves the products containing the variants from Payload. You'll then use this data to update the products in Payload. + +To create the step, create the file `src/workflows/steps/retrieve-payload-items.ts` with the following content: + +```ts title="src/workflows/steps/retrieve-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PAYLOAD_MODULE } from "../../modules/payload" + +type StepInput = { + collection: string; + where: Record; +} + +export const retrievePayloadItemsStep = createStep( + "retrieve-payload-items", + async ({ where, collection }: StepInput, { container }) => { + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + const items = await payloadModuleService.find(collection, { + where, + }) + + return new StepResponse({ + items: items.docs, + }) + } +) +``` + +This step accepts a collection slug and a `where` condition to specify which items to retrieve from Payload, then returns the found items. + +#### Delete Payload Product Variants Workflow + +To create the workflow, create the file `src/workflows/delete-payload-product-variants.ts` with the following content: + +```ts title="src/workflows/delete-payload-product-variants.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { updatePayloadItemsStep } from "./steps/update-payload-items" +import { retrievePayloadItemsStep } from "./steps/retrieve-payload-items" + +type WorkflowInput = { + variant_ids: string[] +} + +export const deletePayloadProductVariantsWorkflow = createWorkflow( + "delete-payload-product-variants", + ({ variant_ids }: WorkflowInput) => { + const retrieveData = transform({ + variant_ids, + }, (data) => { + return { + collection: "products", + where: { + "variants.medusa_id": { + in: data.variant_ids.join(","), + }, + }, + } + }) + + const { items: payloadProducts } = retrievePayloadItemsStep(retrieveData) + + const updateData = transform({ + payloadProducts, + variant_ids, + }, (data) => { + const items = data.payloadProducts.map((payloadProduct) => ({ + id: payloadProduct.id, + variants: payloadProduct.variants.filter((v: any) => !data.variant_ids.includes(v.medusa_id)), + })) + + return { + collection: "products", + items, + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + // Call the step to update the payload items + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product variants to delete from Payload. + +In the workflow, you: + +1. Retrieve the Payload data of the products that the variants belong to using `retrievePayloadItemsStep`. +2. Prepare the data to update the products in Payload by filtering out the variants that should be deleted. +3. Update the products in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Variant Deleted Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-variant.deleted` event is emitted. + +Create the file `src/subscribers/variant-deleted.ts` with the following content: + +```ts title="src/subscribers/variant-deleted.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deletePayloadProductVariantsWorkflow } from "../workflows/delete-payload-product-variants" + +export default async function productVariantDeletedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await deletePayloadProductVariantsWorkflow(container) + .run({ + input: { + variant_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-variant.deleted", +} +``` + +This subscriber listens to the `product-variant.deleted` event and executes the `deletePayloadProductVariantsWorkflow` with the deleted variant's ID. + +#### Test Product Variant Deletion Handling + +To test the product variant deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Delete an existing variant from the product. + +If you check the product in Payload, you should see that the variant has been removed from the product's variants array. + +### e. Handle Product Option Creation + +To handle the `product-option.created` event, you'll create a workflow that adds the new option to the corresponding product in Payload. + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product option from Medusa + +#### Create Payload Product Options Workflow + +You already have the necessary steps, so you only need to create the workflow that uses these steps. + +Create the file `src/workflows/create-payload-product-options.ts` with the following content: + +```ts title="src/workflows/create-payload-product-options.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types" +import { updatePayloadItemsStep } from "./steps/update-payload-items" + +type WorkflowInput = { + option_ids: string[]; +} + +export const createPayloadProductOptionsWorkflow = createWorkflow( + "create-payload-product-options", + ({ option_ids }: WorkflowInput) => { + const { data: productOptions } = useQueryGraphStep({ + entity: "product_option", + fields: [ + "id", + "title", + "product.payload_product.*", + ], + filters: { + id: option_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const updateData = transform({ + productOptions, + }, (data) => { + const items: Record = {} + + data.productOptions.forEach((option) => { + // @ts-expect-error + const payloadProduct = option.product?.payload_product as PayloadCollectionItem + if (!payloadProduct) {return} + + if (!items[payloadProduct.id]) { + items[payloadProduct.id] = { + options: payloadProduct.options || [], + } + } + + // Add the new option to the payload product + const newOption = { + title: option.title, + medusa_id: option.id, + } + + // Check if option already exists, if not add it + const existingOptionIndex = items[payloadProduct.id].options.findIndex( + (o: any) => o.medusa_id === option.id + ) + + if (existingOptionIndex === -1) { + items[payloadProduct.id].options.push(newOption) + } + }) + + return { + collection: "products", + items: Object.keys(items).map((id) => ({ + id, + ...items[id], + })), + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product options to add to Payload. + +In the workflow, you: + +1. Retrieve the product option details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload. +2. Prepare the data to update the product in Payload by adding the new option to the existing options array, checking if it doesn't already exist. +3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Option Created Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-option.created` event is emitted. + +Create the file `src/subscribers/option-created.ts` with the following content: + +```ts title="src/subscribers/option-created.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductOptionsWorkflow } from "../workflows/create-payload-product-options" + +export default async function productOptionCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await createPayloadProductOptionsWorkflow(container) + .run({ + input: { + option_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-option.created", +} +``` + +This subscriber listens to the `product-option.created` event and executes the `createPayloadProductOptionsWorkflow` with the created option's ID. + +#### Test Product Option Creation Handling + +To test the product option creation handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Add a new option to the product and save the changes. + +If you check the product in Payload, you should see that the new option has been added to the product's options array. + +### f. Handle Product Option Deletions + +To handle the `product-option.deleted` event, you'll create a workflow that removes the option from the corresponding product in Payload. + +The workflow will have the following steps: + +- [retrievePayloadItemsStep](#retrievePayloadItemsStep): Retrieve the products containing the option from Payload + +#### Delete Payload Product Options Workflow + +You already have the necessary steps, so you only need to create the workflow that uses these steps. + +Create the file `src/workflows/delete-payload-product-options.ts` with the following content: + +```ts title="src/workflows/delete-payload-product-options.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { updatePayloadItemsStep } from "./steps/update-payload-items" +import { retrievePayloadItemsStep } from "./steps/retrieve-payload-items" + +type WorkflowInput = { + option_ids: string[] +} + +export const deletePayloadProductOptionsWorkflow = createWorkflow( + "delete-payload-product-options", + ({ option_ids }: WorkflowInput) => { + const retrieveData = transform({ + option_ids, + }, (data) => { + return { + collection: "products", + where: { + "options.medusa_id": { + in: data.option_ids.join(","), + }, + }, + } + }) + + const { items: payloadProducts } = retrievePayloadItemsStep(retrieveData) + + const updateData = transform({ + payloadProducts, + option_ids, + }, (data) => { + const items = data.payloadProducts.map((payloadProducts) => ({ + id: payloadProducts.id, + options: payloadProducts.options.filter((o: any) => !data.option_ids.includes(o.medusa_id)), + variants: payloadProducts.variants.map((variant: any) => ({ + ...variant, + option_values: variant.option_values.filter((ov: any) => !data.option_ids.includes(ov.medusa_option_id)), + })), + })) + + return { + collection: "products", + items, + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product options to delete from Payload. + +In the workflow, you: + +1. Retrieve the products that contain the options to be deleted using the `retrievePayloadItemsStep`. +2. Prepare the data to update the products in Payload by filtering out the options that should be deleted. +3. Update the products in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Option Deleted Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-option.deleted` event is emitted. + +Create the file `src/subscribers/option-deleted.ts` with the following content: + +```ts title="src/subscribers/option-deleted.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deletePayloadProductOptionsWorkflow } from "../workflows/delete-payload-product-options" + +export default async function productOptionDeletedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await deletePayloadProductOptionsWorkflow(container) + .run({ + input: { + option_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-option.deleted", +} +``` + +This subscriber listens to the `product-option.deleted` event and executes the `deletePayloadProductOptionsWorkflow` with the deleted option's ID. + +#### Test Product Option Deletion Handling + +To test the product option deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Delete an existing option from the product. + +If you check the product in Payload, you should see that the option has been removed from the product's options array. + +*** + +## Next Steps + +You've successfully integrated Medusa with Payload to manage content related to products, variants, and options. You can expand on this integration by adding more features, such as: + +1. Managing the content of other entities, like categories or collections. The process is similar to what you've done for products: + 1. Create a collection in Payload for the entity. + 2. Create Medusa workflows and subscribers to handle the creation, update, and deletion of the entity. + 3. Display the payload data in your Next.js Starter Storefront. +2. Enable [localization](https://payloadcms.com/docs/configuration/localization) in Payload to support multiple languages. + - You only need to manage the localized content in Payload. Only the default locale will be synced with Medusa. + - You can show the localized content in your Next.js Starter Storefront based on the customer's locale. +3. Add custom fields to the Payload collections. For example, you can add images to product variants and display them in the Next.js Starter Storefront. + +### Learn More about Medusa + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + +### Troubleshooting + +If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md). + +### Getting Help + +If you encounter issues not covered in the troubleshooting guides: + +1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions. +2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members. + + # Integrate Medusa with Resend (Email Notifications) In this guide, you'll learn how to integrate Medusa with Resend. @@ -79470,6 +82639,7 @@ Learn how to integrate a custom third-party authentication provider in the [Crea Integrate a third-party Content-Management System (CMS) to utilize rich content-related features. - [Contentful (Localization)](https://docs.medusajs.com/integrations/guides/contentful/index.html.md) +- [Payload CMS](https://docs.medusajs.com/integrations/guides/payload/index.html.md) - [Sanity](https://docs.medusajs.com/integrations/guides/sanity/index.html.md) *** diff --git a/www/apps/resources/app/integrations/guides/payload/page.mdx b/www/apps/resources/app/integrations/guides/payload/page.mdx new file mode 100644 index 0000000000..487ef53f85 --- /dev/null +++ b/www/apps/resources/app/integrations/guides/payload/page.mdx @@ -0,0 +1,3433 @@ +--- +sidebar_label: "Integrate Payload" +tags: + - server + - tutorial + - product +products: + - product +--- + +import { Card, Prerequisites, Details, WorkflowDiagram, H3 } from "docs-ui" +import { Github } from "@medusajs/icons" + +export const metadata = { + title: `Integrate Payload CMS with Medusa`, +} + +# {metadata.title} + +In this tutorial, you'll learn how to integrate [Payload](https://payloadcms.com/) with Medusa. + +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa also facilitates integrating third-party services that enrich your application with features specific to your unique business use case. + +By integrating Payload, you can manage your products' content with powerful content management capabilities, such as managing custom fields, media, localization, and more. + +## Summary + +By following this tutorial, you'll learn how to: + +- Install and set up Medusa. +- Set up Payload in the Next.js Starter Storefront. +- Integrate Payload with Medusa to sync product data. + - You'll sync product data when triggered manually by admin users, or as a result of product events in Medusa. +- Display product data from Payload in the Next.js Starter Storefront. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +![Diagram showcasing a flowchart of interactions between customer, admin, Medusa, and Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754551568/Medusa%20Resources/payload-summary_ndeiw0.jpg) + + + +--- + +## Step 1: Install a Medusa Application + + + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](../../../nextjs-starter/page.mdx), choose "Yes." + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`. + + + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/fundamentals/api-routes). Learn more in [Medusa's Architecture documentation](!docs!/learn/introduction/architecture). + + + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard. + + + +Check out the [troubleshooting guides](../../../troubleshooting/create-medusa-app-errors/page.mdx) for help. + + + +--- + +## Step 2: Set Up Payload in the Next.js Starter Storefront + +In this step, you'll set up Payload in the Next.js Starter Storefront. This requires installing the necessary dependencies, configuring Payload, and creating collections for products and other types. + +### a. Install Dependencies + +In the directory of the Next.js Starter Storefront, run the following command to install the necessary dependencies: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm install payload @payloadcms/next @payloadcms/richtext-lexical sharp @payloadcms/db-postgres +``` + +### b. Add Resolution for `undici` + +Payload uses the `undici` package, but some versions of it cause an error in the Payload CLI. + +To avoid these errors, add the following resolution and override to the `package.json` file of the Next.js Starter Storefront: + +```json title="package.json" badgeLabel="Storefront" badgeColor="blue" +{ + "resolutions": { + // other resolutions... + "undici": "5.20.0" + }, + "overrides": { + // other overrides... + "undici": "5.20.0" + } +} +``` + +Then, re-install the dependencies to ensure the correct version of `undici` is used: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm install +``` + +### c. Copy Payload Template Files + +Next, you'll need to copy the Payload template files into the Next.js Starter Storefront. These files allow you to access the Payload admin from the Next.js Starter Storefront. + +You can find the files in the [examples GitHub repository](https://github.com/medusajs/examples/tree/main/payload-integration/storefront/src/app/(payload)). Copy these files into a new `src/app/(payload)` directory in the Next.js Starter Storefront. + +Then, move all previous files that were under the `src/app` directory into a new `src/app/(storefront)` directory. This will ensure that the Payload admin is accessible at the `/admin` route, and the storefront is still accessible at the root route. + +So, the `src/app` directory should now only include the `(payload)` and `(storefront)` directories, each containing their respective files. + +![Overview of the Next.js Starter Storefront directory structure of the src directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1754474683/Medusa%20Resources/payload-storefront-dir_lt7yyw.jpg) + +### d. Modify Next.js Middleware + +The Next.js Starter Storefront uses a middleware to prefix all route paths with the first region's country code. While this is useful for storefront routes, it's unnecessary for the Payload admin routes. + +So, you'll modify the middleware to exclude the `/admin` routes. + +In `src/middleware.ts`, change the `config` object to include `/admin` in the `matcher` regex pattern: + +```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" +export const config = { + matcher: [ + "/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp|admin).*)", + ], +} +``` + +### e. Add Payload Configuration + +Next, you'll add the necessary configuration to run Payload in the Next.js Starter Storefront. + +Create the file `src/payload.config.ts` with the following content: + +```ts title="src/payload.config.ts" badgeLabel="Storefront" badgeColor="blue" +import sharp from "sharp" +import { lexicalEditor } from "@payloadcms/richtext-lexical" +import { postgresAdapter } from "@payloadcms/db-postgres" +import { buildConfig } from "payload" + +export default buildConfig({ + editor: lexicalEditor(), + collections: [ + // TODO add collections + ], + + secret: process.env.PAYLOAD_SECRET || "", + db: postgresAdapter({ + pool: { + connectionString: process.env.PAYLOAD_DATABASE_URL || "", + }, + }), + sharp, +}) +``` + +The configurations are mostly default Payload configurations. You configure Payload to use PostgreSQL as the database adapter. Later, you'll add collections for products and other types. + + + +Refer to the [Payload documentation](https://payloadcms.com/docs/configuration/overview) for more information on configuring Payload. + + + +In the configurations, you use two environment variables. To set them, add the following in your storefront's `.env.local` file: + +```shell title=".env.local" badgeLabel="Storefront" badgeColor="blue" +PAYLOAD_DATABASE_URL=postgres://postgres:@localhost:5432/payload +PAYLOAD_SECRET=supersecret +``` + +Where: + +- `PAYLOAD_DATABASE_URL` is the connection string to the PostgreSQL database that Payload will use. You don't need to create the database beforehand, as Payload will create it automatically. +- `PAYLOAD_SECRET` is your Payload secret. In production, you should use a complex and secure string. + +You also need to add a path alias to the `payload.config.ts` file, as Payload will try to import it using `@payload-config`. + +In `tsconfig.json`, add the following path alias: + +```json title="tsconfig.json" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"]]} +{ + "compilerOptions": { + // other options... + "paths": { + // other paths... + "@payload-config": ["./payload.config.ts"] + } + } +} +``` + +The `baseUrl` in the `tsconfig.json` file is set to `"./src"`, so the path alias will resolve to `src/payload.config.ts`. + +### f. Customize Next.js Configurations + +You also need to customize the Next.js configurations to ensure that Payload works correctly with the Next.js Starter Storefront. + +In `next.config.js`, add the following `require` statement at the top of the file: + +```js title="next.config.js" badgeLabel="Storefront" badgeColor="blue" +const { withPayload } = require("@payloadcms/next/withPayload") +``` + +Then, find the `module.exports` statement and replace it with the following: + +```js title="next.config.js" badgeLabel="Storefront" badgeColor="blue" +module.exports = withPayload(nextConfig) +``` + +You wrap the Next.js configuration with the `withPayload` function to ensure that Payload works correctly with Next.js. + +### g. Add Collections to Payload + +Now that Payload is set up in your storefront, you'll create the following [collections](https://payloadcms.com/docs/configuration/collections): + +- `User`: A Payload user with API key authentication, allowing you later to sync product data from Medusa to Payload. +- `Media`: A collection for media files, allowing you to manage product images and other media. +- `Product`: A collection for products, which will be synced with Medusa's product data. + +#### User Collection + +To create the `User` collection, create the file `src/collections/Users.ts` with the following content: + +```ts title="src/collections/Users.ts" badgeLabel="Storefront" badgeColor="blue" +import type { CollectionConfig } from "payload" + +export const Users: CollectionConfig = { + slug: "users", + admin: { + useAsTitle: "email", + }, + auth: { + useAPIKey: true, + }, + fields: [], +} +``` + +The `Users` collection allows you to manage users that can log into the Payload admin with email and API key authentication. + + + +Refer to the [Payload documentation](https://payloadcms.com/docs/authentication/api-keys) to learn more about API key authentication. + + + +#### Media Collection + +To create the `Media` collection, create the file `src/collections/Media.ts` with the following content: + +```ts title="src/collections/Media.ts" badgeLabel="Storefront" badgeColor="blue" +import { CollectionConfig } from "payload" + +export const Media: CollectionConfig = { + slug: "media", + upload: { + staticDir: "public", + imageSizes: [ + { + name: "thumbnail", + width: 400, + height: 300, + position: "centre", + }, + { + name: "card", + width: 768, + height: 1024, + position: "centre", + }, + { + name: "tablet", + width: 1024, + height: undefined, + position: "centre", + }, + ], + adminThumbnail: "thumbnail", + mimeTypes: ["image/*"], + pasteURL: { + allowList: [ + { + protocol: "http", + hostname: "localhost", + }, + { + protocol: "https", + hostname: "medusa-public-images.s3.eu-west-1.amazonaws.com", + }, + { + protocol: "https", + hostname: "medusa-server-testing.s3.amazonaws.com", + }, + { + protocol: "https", + hostname: "medusa-server-testing.s3.us-east-1.amazonaws.com", + }, + ], + }, + }, + fields: [ + { + name: "alt", + type: "text", + label: "Alt Text", + required: false, + }, + ], +} +``` + +The `Media` collection will store media files, such as product images. You can upload files to the [Storage Adapters](https://payloadcms.com/docs/upload/storage-adapters) configured in Payload, such as AWS S3 or local storage. The above configurations point to the `public` directory of the Next.js Starter Storefront as the upload directory. + +Note that you allow pasting URLs from specific sources, such as the Medusa public images S3 bucket. This allows you to paste Medusa's stock image URLs in the Payload admin. + +#### Product Collection + +Finally, you'll add the `Product` collection, which will be synced with Medusa's product data. + +Create the file `src/collections/Products.ts` with the following content: + +export const productCollectionHighlights = [ + ["21", "update", "Only allow updating from Medusa"], + ["144", "update", "Only allow updating from Medusa"], + ["173", "update", "Only allow updating from Medusa"], + ["190", "update", "Only allow updating from Medusa"], + ["203", "update", "Only allow updating from Medusa"], + ["224", "create", "Only allow creating from Medusa"], + ["225", "delete", "Only allow deleting from Medusa"] +] + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={productCollectionHighlights} +import { CollectionConfig } from "payload" +import { payloadMedusaSdk } from "../lib/payload-sdk" + +export const Products: CollectionConfig = { + slug: "products", + admin: { + useAsTitle: "title", + }, + fields: [ + { + name: "medusa_id", + type: "text", + label: "Medusa Product ID", + required: true, + unique: true, + admin: { + description: "The unique identifier from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "title", + type: "text", + label: "Title", + required: true, + admin: { + description: "The product title", + }, + }, + { + name: "handle", + type: "text", + label: "Handle", + required: true, + admin: { + description: "URL-friendly unique identifier", + }, + validate: (value: any) => { + // validate URL-friendly handle + if (typeof value !== "string") { + return "Handle must be a string" + } + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) { + return "Handle must be URL-friendly (lowercase letters, numbers, and hyphens only)" + } + return true + }, + }, + { + name: "subtitle", + type: "text", + label: "Subtitle", + required: false, + admin: { + description: "Product subtitle", + }, + }, + { + name: "description", + type: "richText", + label: "Description", + required: false, + admin: { + description: "Detailed product description", + }, + }, + { + name: "thumbnail", + type: "upload", + relationTo: "media" as any, + label: "Thumbnail", + required: false, + admin: { + description: "Product thumbnail image", + }, + }, + { + name: "images", + type: "array", + label: "Product Images", + required: false, + fields: [ + { + name: "image", + type: "upload", + relationTo: "media" as any, + required: true, + }, + ], + admin: { + description: "Gallery of product images", + }, + }, + { + name: "seo", + type: "group", + label: "SEO", + fields: [ + { + name: "meta_title", + type: "text", + label: "Meta Title", + required: false, + }, + { + name: "meta_description", + type: "textarea", + label: "Meta Description", + required: false, + }, + { + name: "meta_keywords", + type: "text", + label: "Meta Keywords", + required: false, + }, + ], + admin: { + description: "SEO-related fields for better search visibility", + }, + }, + { + name: "options", + type: "array", + fields: [ + { + name: "title", + type: "text", + label: "Option Title", + required: true, + }, + { + name: "medusa_id", + type: "text", + label: "Medusa Option ID", + required: true, + admin: { + description: "The unique identifier for the option from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + ], + validate: (value: any, { req, previousValue }) => { + // TODO add validation to ensure that the number of options cannot be changed + }, + }, + { + name: "variants", + type: "array", + fields: [ + { + name: "title", + type: "text", + label: "Variant Title", + required: true, + }, + { + name: "medusa_id", + type: "text", + label: "Medusa Variant ID", + required: true, + admin: { + description: "The unique identifier for the variant from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "option_values", + type: "array", + fields: [ + { + name: "medusa_id", + type: "text", + label: "Medusa Option Value ID", + required: true, + admin: { + description: "The unique identifier for the option value from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "medusa_option_id", + type: "text", + label: "Medusa Option ID", + required: true, + admin: { + description: "The unique identifier for the option from Medusa", + hidden: true, // Hide this field in the admin UI + }, + access: { + update: ({ req }) => !!req.query.is_from_medusa, + }, + }, + { + name: "value", + type: "text", + label: "Value", + required: true, + }, + ], + }, + ], + validate: (value: any, { req, previousValue }) => { + // TODO add validation to ensure that the number of variants cannot be changed + }, + }, + ], + hooks: { + // TODO add + }, + access: { + create: ({ req }) => !!req.query.is_from_medusa, + delete: ({ req }) => !!req.query.is_from_medusa, + }, +} +``` + +You create a `Products` collection having the following fields: + +- `medusa_id`: The product's ID in Medusa, which is useful when syncing data between Payload and Medusa. +- `title`: The product's title. +- `handle`: A URL-friendly unique identifier for the product. +- `subtitle`: An optional subtitle for the product. +- `description`: A rich text description of the product. +- `thumbnail`: An optional thumbnail image for the product. +- `images`: An array of images for the product. +- `seo`: A group of fields for SEO-related information, such as meta title, description, and keywords. +- `options`: An array of product options, such as size or color. +- `variants`: An array of product variants, each with its own title and option values. + +All of these fields will be filled from Medusa, and will be synced back to Medusa when the product is updated in Payload. + +In addition, you also add the following [access-control](https://payloadcms.com/docs/access-control/overview) configurations: + +- You disallow creating or deleting products from the Payload admin, as these actions should only be performed from Medusa. +- You disallow updating the `medusa_id` fields from the Payload admin, as these fields are managed by Medusa. + +#### Add Validation for Options and Variants + +Payload admin users can only manage the content of product options and variants, but they shouldn't be able to remove or add new options or variants. + +To ensure this behavior, you'll add validation to the `options` and `variants` fields in the `Products` collection. + +First, replace the `validate` function in the `options` field with the following: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +export const Products: CollectionConfig = { + // other configurations... + fields: [ + // other fields... + { + name: "options", + // other configurations... + validate: (value: any, { req, previousValue }) => { + if (req.query.is_from_medusa) { + return true // Skip validation if the request is from Medusa + } + + if (!Array.isArray(value)) { + return "Options must be an array" + } + + const optionsChanged = value.length !== previousValue?.length || value.some((option) => { + return !option.medusa_id || !previousValue?.some( + (prevOption) => (prevOption as any).medusa_id === option.medusa_id + ) + }) + + // Prevent update if the number of options is changed + return !optionsChanged || "Options cannot be changed in number" + }, + }, + ], +} +``` + +If the request is from Medusa (which is indicated by the `is_from_medusa` query parameter), the validation is skipped. + +Otherwise, you only allow updating the options if the number of options remains the same and each option has a `medusa_id` that matches an existing option in the previous value. + +Next, replace the `validate` function in the `variants` field with the following: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +export const Products: CollectionConfig = { + // other configurations... + fields: [ + // other fields... + { + name: "variants", + // other configurations... + validate: (value: any, { req, previousValue }) => { + if (req.query.is_from_medusa) { + return true // Skip validation if the request is from Medusa + } + + if (!Array.isArray(value)) { + return "Variants must be an array" + } + + const changedVariants = value.length !== previousValue?.length || value.some((variant: any) => { + return !variant.medusa_id || !previousValue?.some( + (prevVariant: any) => prevVariant.medusa_id === variant.medusa_id + ) + }) + + if (changedVariants) { + // Prevent update if the number of variants is changed + return "Variants cannot be changed in number" + } + + const changedOptionValues = value.some((variant: any) => { + if (!Array.isArray(variant.option_values)) { + return true // Invalid structure + } + + const previousVariant = previousValue?.find( + (v: any) => v.medusa_id === variant.medusa_id + ) as Record | undefined + + return variant.option_values.length !== previousVariant?.option_values.length || + variant.option_values.some((optionValue: any) => { + return !optionValue.medusa_id || !previousVariant?.option_values.some( + (prevOptionValue: any) => prevOptionValue.medusa_id === optionValue.medusa_id + ) + }) + }) + + return !changedOptionValues || "Option values cannot be changed in number" + }, + }, + ], +} +``` + +If the request is from Medusa, the validation is skipped. + +Otherwise, the function validates that: + +- The number of variants is the same as the previous value. +- Each variant has a `medusa_id` that matches an existing variant in the previous value. +- The number of option values for each variant is the same as the previous value. +- Each option value has a `medusa_id` that matches an existing option value in the previous value. + +If any of these validations fail, an error message is returned, preventing the update. + +### h. Add Hooks to Sync Product Data + +Next, you'll add a `beforeChange` hook to the `Products` collection that will normalize incoming `description` data to [rich-text format](https://payloadcms.com/docs/rich-text/overview). + +In `src/collections/Products.ts`, add the following import statement at the top of the file: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +import { convertLexicalToMarkdown, convertMarkdownToLexical, editorConfigFactory } from "@payloadcms/richtext-lexical" +``` + +Then, in the `Products` collection, add a `beforeChange` property to the `hooks` configuration: + +```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" +export const Products: CollectionConfig = { + // other configurations... + hooks: { + beforeChange: [ + async ({ data, req }) => { + if (typeof data.description === "string") { + data.description = convertMarkdownToLexical({ + editorConfig: await editorConfigFactory.default({ + config: req.payload.config, + }), + markdown: data.description, + }) + } + + return data + }, + ], + }, +} +``` + +This hook checks if the `description` field is a string and converts it to rich-text format. This ensures that a description coming from Medusa is properly formatted when stored in Payload. + +## i. Generate Payload Imports Map + +Before running the Payload admin, you need to generate the imports map that Payload uses to resolve the collections and other configurations. + +Run the following command in the Next.js Starter Storefront directory: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npx payload generate:importmap +``` + +This command generates the `src/app/(payload)/admin/importMap.js` file that Payload needs. + +## j. Run the Payload Admin + +You can now run the Payload admin in the Next.js Starter Storefront and create an admin user. + +To start the Next.js Starter Storefront, run the following command in the Next.js Starter Storefront directory: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +Then, open the Payload admin in your browser at `http://localhost:8000/admin`. The first time you access it, Payload will create a database at the connection URL you provided in the `.env.local` file. + +Then, you'll see a form to create a new admin user. Enter the user's credentials and submit the form. + +Once you're logged in, you can see the `Products`, `Users`, and `Media` collections in the Payload admin. + +![Payload Admin Dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1754477731/Medusa%20Resources/CleanShot_2025-08-06_at_13.55.06_2x_bhdkkd.png) + +--- + +## Step 3: Integrate Payload with Medusa + +Now that Payload is set up in the Next.js Starter Storefront, you'll create a Payload [Module](!docs!/learn/fundamentals/modules) to integrate it with Medusa. + +A module is a reusable package that provides functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + + + +Refer to the [Modules](!docs!/learn/fundamentals/modules) documentation to learn more about modules and their structure. + + + +### a. Create Module Directory + +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/payload`. + +### b. Create Types for the Module + +Next, you'll create a types file that will hold the types for the module's options and service methods. + +Create the file `src/modules/payload/types.ts` with the following content: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadModuleOptions { + serverUrl: string; + apiKey: string; + userCollection?: string; +} +``` + +For now, the file only contains the `PayloadModuleOptions` interface, which defines the options that the module will receive. It includes: + +- `serverUrl`: The URL of the Payload server. +- `apiKey`: The API key for authenticating with the Payload server. +- `userCollection`: The name of the user collection in Payload. This is optional and defaults to `users`. It's useful for the authentication header when sending requests to the Payload API. + +### c. Create Service + +A module has a service that contains its logic. So, the Payload Module's service will contain the logic to create, update, retrieve, and delete data in Payload. + +Create the file `src/modules/payload/service.ts` with the following content: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadModuleOptions, +} from "./types" +import { MedusaError } from "@medusajs/framework/utils" + +type InjectedDependencies = { + // inject any dependencies you need here +}; + +export default class PayloadModuleService { + private baseUrl: string + private headers: Record + private defaultOptions: Record = { + is_from_medusa: true, + } + + constructor( + container: InjectedDependencies, + options: PayloadModuleOptions + ) { + this.validateOptions(options) + this.baseUrl = `${options.serverUrl}/api` + + this.headers = { + "Content-Type": "application/json", + "Authorization": `${ + options.userCollection || "users" + } API-Key ${options.apiKey}`, + } + } + + validateOptions(options: Record): void | never { + if (!options.serverUrl) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Payload server URL is required" + ) + } + + if (!options.apiKey) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "Payload API key is required" + ) + } + } +} +``` + +The constructor of a module's service receives the following parameters: + +1. The [Module container](!docs!/learn/fundamentals/modules/container) that allows you to resolve module and Framework resources. You don't need to resolve any resources in this module, so you can leave it empty. +2. The module options, which you'll [pass to the module when you register it later](#e-add-module-to-medusas-configurations) in the Medusa application. + +In the constructor, you validate the module options and set up the Payload base URL and headers that are necessary to send requests to Payload. + +### c. Add Methods to the Service + +Next, you'll add methods to the service that allow you to create, update, retrieve, and delete products in Payload. + +#### makeRequest Method + +The `makeRequest` private method is a utility function that makes HTTP requests to the Payload API. You'll use this method in other public methods that perform operations in Payload. + +Add the `makeRequest` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}` + + try { + const response = await fetch(url, { + ...options, + headers: { + ...this.headers, + ...options.headers, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Payload API error: ${response.status} ${response.statusText}. ${ + errorData.message || "" + }` + ) + } + + return await response.json() + } catch (error) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Failed to communicate with Payload: ${JSON.stringify(error)}` + ) + } + } +} +``` + +The `makeRequest` method receives the endpoint to call and the options for the request. It constructs the full URL, makes the request, and returns the response data as JSON. + +If the request fails, it throws a `MedusaError` with the error message. + +#### create Method + +The `create` method will allow you to create an entry in a Payload collection, such as `Products`. + +Before you create the method, you'll need to add necessary types for its parameters and return value. + +In `src/modules/payload/types.ts`, add the following types: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadCollectionItem { + id: string; + createdAt: string; + updatedAt: string; + medusa_id: string; + [key: string]: any; +} + +export interface PayloadUpsertData { + [key: string]: any; +} + +export interface PayloadQueryOptions { + depth?: number; + locale?: string; + fallbackLocale?: string; + select?: string; + populate?: string; + limit?: number; + page?: number; + sort?: string; + where?: Record; +} + +export interface PayloadItemResult { + doc: T; + message: string; +} +``` + +You define the following types: + +- `PayloadCollectionItem`: an item in a Payload collection. +- `PayloadUpsertData`: the data required to create or update an item in a Payload collection. +- `PayloadQueryOptions`: the options for querying items in a Payload collection, which you can learn more about in the [Payload documentation](https://payloadcms.com/docs/queries/overview). +- `PayloadItemResult`: the result of a querying or performing an operation on a Payload item, which includes the item and a message. + +Next, add the following import statements at the top of the `src/modules/payload/service.ts` file: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadCollectionItem, + PayloadUpsertData, + PayloadQueryOptions, + PayloadItemResult, +} from "./types" +import qs from "qs" +``` + +You import the types you just defined and the `qs` library, which you'll use to stringify query options. + +Then, add the `create` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async create( + collection: string, + data: PayloadUpsertData, + options: PayloadQueryOptions = {} + ): Promise> { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}/${stringifiedQuery}` + + const result = await this.makeRequest>(endpoint, { + method: "POST", + body: JSON.stringify(data), + }) + return result + } +} +``` + +The `create` method receives the following parameters: + +- `collection`: the slug of the collection in Payload where you want to create an item. For example, `products`. +- `data`: the data for the new item you want to create. +- `options`: optional query options for the request. + +In the method, you use the `makeRequest` method to send a `POST` request to Payload, passing it the endpoint and request body data. + +Finally, you return the result of the request that contains the created item and a message. + +#### update Method + +Next, you'll add the `update` method that allows you to update an existing item in a Payload collection. + +Add the `update` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async update( + collection: string, + data: PayloadUpsertData, + options: PayloadQueryOptions = {} + ): Promise> { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}/${stringifiedQuery}` + + const result = await this.makeRequest>(endpoint, { + method: "PATCH", + body: JSON.stringify(data), + }) + + return result + } +} +``` + +Similar to the `create` method, the `update` method receives the collection slug, the data to update, and optional query options. + +In the method, you use the `makeRequest` method to send a `PATCH` request to Payload, passing it the endpoint and request body data. + +Finally, you return the result of the request that contains the updated item and a message. + +#### delete Method + +Next, you'll add the `delete` method that allows you to delete an item from a Payload collection. + +First, add the following type to `src/modules/payload/types.ts`: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadApiResponse { + data?: T; + errors?: Array<{ + message: string; + field?: string; + }>; + message?: string; +} +``` + +This represents a generic response from Payload, which can include data, errors, and a message. + +Then, add the following import statement at the top of the `src/modules/payload/service.ts` file: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadApiResponse, +} from "./types" +``` + +After that, add the `delete` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async delete( + collection: string, + options: PayloadQueryOptions = {} + ): Promise { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}/${stringifiedQuery}` + + const result = await this.makeRequest(endpoint, { + method: "DELETE", + }) + + return result + } +} +``` + +The `delete` method receives as parameters the collection slug and optional query options. + +In the method, you use the `makeRequest` method to send a `DELETE` request to Payload, passing it the endpoint. + +Finally, you return the result of the request that contains any data, errors, or a message. + +#### find Method + +The last method you'll add for now is the `find` method, which allows you to retrieve items from a Payload collection. + +First, add the following type to `src/modules/payload/types.ts`: + +```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green" +export interface PayloadBulkResult { + docs: T[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + pagingCounter: number; +} +``` + +This type represents the result of a bulk query to a Payload collection, which includes an array of documents and pagination information. + +Then, add the following import statement at the top of the `src/modules/payload/service.ts` file: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +import { + PayloadBulkResult, +} from "./types" +``` + +After that, add the `find` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + async find( + collection: string, + options: PayloadQueryOptions = {} + ): Promise> { + + const stringifiedQuery = qs.stringify({ + ...options, + ...this.defaultOptions, + }, { + addQueryPrefix: true, + }) + + const endpoint = `/${collection}${stringifiedQuery}` + + const result = await this.makeRequest< + PayloadBulkResult + >(endpoint) + + return result + } +} +``` + +The `find` method receives the collection slug and optional query options. + +In the method, you use the `makeRequest` method to send a `GET` request to Payload, passing it the endpoint with the query options. + +Finally, you return the result of the request that contains an array of documents and pagination information. + +### d. Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders. + +To create the module's definition, create the file `src/modules/payload/index.ts` with the following content: + +```ts title="src/modules/payload/index.ts" badgeLabel="Medusa application" badgeColor="green" +import { Module } from "@medusajs/framework/utils" +import PayloadModuleService from "./service" + +export const PAYLOAD_MODULE = "payload" + +export default Module(PAYLOAD_MODULE, { + service: PayloadModuleService, +}) +``` + +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `payload`. +2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts. + +Aside from the module definition, you export the module's name as `PAYLOAD_MODULE` so you can reference it later. + +### e. Add Module to Medusa's Configurations + +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: + +```ts title="medusa-config.ts" badgeLabel="Medusa application" badgeColor="green" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/payload", + options: { + serverUrl: process.env.PAYLOAD_SERVER_URL || "http://localhost:8000", + apiKey: process.env.PAYLOAD_API_KEY, + userCollection: process.env.PAYLOAD_USER_COLLECTION || "users", + }, + }, + ], +}) +``` + +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name. + +You also pass an `options` property with the module's options. You'll set the values of these options next. + +### f. Set Environment Variables + +To use the Payload Module, you need to set the module options in the environment variables of your Medusa application. + +One of these options is the API key of a Payload admin user. To get the API key: + +1. Start the Next.js Starter Storefront with the following command: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +2. Open `localhost:8000/admin` in your browser and log in with the admin user you created earlier. +3. Click on the "Users" collection in the sidebar. +4. Choose your admin user from the list. +5. Click on the "Enable API key" checkbox and copy the API key that appears. +6. Click the "Save" button to save the changes. + +![The user form with the API key enabled](https://res.cloudinary.com/dza7lstvk/image/upload/v1754479684/Medusa%20Resources/CleanShot_2025-08-06_at_14.27.25_2x_gasihl.png) + +Next, add the following environment variables to your Medusa application's `.env` file: + +```shell title=".env" badgeLabel="Medusa application" badgeColor="green" +PAYLOAD_SERVER_URL=http://localhost:8000 +PAYLOAD_API_KEY=your_api_key_here +PAYLOAD_USER_COLLECTION=users +``` + +Make sure to replace `your_api_key_here` with the API key you copied from the Payload admin. + +The Payload Module is now ready for use. You'll add customizations next to sync product data between Medusa and Payload. + +--- + +## Step 4: Create Virtual Read-Only Link to Products + +Medusa's [Module Links](!docs!/learn/fundamentals/module-links) feature allows you to virtually link data models from external services to modules in your Medusa application. Then, when you retrieve data from Medusa, you can also retrieve the linked data from the third-party service automatically. + +In this step, you'll define a virtual read-only link between the `Products` collection in Payload and the `Product` model in Medusa. Later, you'll be able to retrieve products from Payload while retrieving products in Medusa. + +### a. Define the Link + +To define a virtual read-only link, create the file `src/links/product-payload.ts` with the following content: + +```ts title="src/links/product-payload.ts" badgeLabel="Medusa application" badgeColor="green" +import { defineLink } from "@medusajs/framework/utils" +import ProductModule from "@medusajs/medusa/product" +import { PAYLOAD_MODULE } from "../modules/payload" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + }, + { + linkable: { + serviceName: PAYLOAD_MODULE, + alias: "payload_product", + primaryKey: "product_id", + }, + }, + { + readOnly: true, + } +) +``` + +The `defineLink` function accepts three parameters: + +- An object of the first data model that is part of the link. In this case, it's the `Product` model from Medusa's Product Module. +- An object of the second data model that is part of the link. In this case, it's the `Products` collection from the Payload Module. You set the following properties: + - `serviceName`: the name of the Payload Module, which is `payload`. + - `alias`: an alias for the linked data model, which is `payload_product`. You'll use this alias to reference the linked data model in queries. + - `primaryKey`: the primary key of the linked data model, which is `product_id`. Medusa will look for this field in the retrieved `Products` from payload to match it with the `id` field of the `Product` model. +- An object with the `readOnly` property set to `true`, indicating that this link is read-only. This means you can only retrieve the linked data, but you don't manage the link in the database. + +### b. Add list Method to the Payload Module Service + +When you retrieve products from Medusa with their `payload_product` link, Medusa will call the `list` method of the Payload Module's service to retrieve the linked products from Payload. + +So, in `src/modules/payload/service.ts`, add a `list` method to the `PayloadModuleService` class: + +```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green" +export default class PayloadModuleService { + // ... other methods + async list( + filter: { + product_id: string | string[] + } + ) { + const collection = filter.product_id ? "products" : "unknown" + const ids = Array.isArray(filter.product_id) ? filter.product_id : [filter.product_id] + const result = await this.find( + collection, + { + where: { + medusa_id: { + in: ids.join(","), + }, + }, + depth: 2, + } + ) + + return result.docs.map((doc) => ({ + ...doc, + product_id: doc.medusa_id, + })) + } +} +``` + +The `list` method receives a `filter` object with an `product_id` property, which is the Medusa product ID(s) to retrieve their corresponding data from Payload. + +In the method, you call the `find` method of the Payload Module's service to retrieve products from the `products` collection in Payload. You pass a `where` query parameter to filter products by their `medusa_id` field. + +Finally, you return an array of the payload products. You set the `product_id` field to the value of the `medusa_id` field, which is used to match the linked data in Medusa. + +You can now retrieve products from Payload while retrieving products in Medusa. You'll learn how to do this in the upcoming steps. + + + +The `list` method is implemented to be re-usable with different collections and data models. For example, if you add a `Categories` collection in Payload, you can use the same `list` method to retrieve categories by their `medusa_id` field. In that case, the `filter` object would have a `category_id` property instead of `product_id`, and you can set the `collection` variable to `"categories"`. + + + +--- + +## Step 5: Create Payload Product Workflow + +In this step, you'll create the functionality to create a Medusa product in Payload. You'll later execute that functionality either when triggered by an admin user, or automatically when a product is created in Medusa. + +You create custom commerce features in [workflows](!docs!/learn/fundamentals/workflows). A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features. + + + +Refer to the [Workflows documentation](!docs!/learn/fundamentals/workflows) to learn more. + + + +The workflow to create a Payload product will have the following steps: + + + +You only need to create the `createPayloadItemsStep`, as the other two steps are already available in Medusa. + +### createPayloadItemsStep + +The `createPayloadItemsStep` will create an item in a Payload collection, such as `Products`. + +To create the step, create the file `src/workflows/steps/create-payload-items.ts` with the following content: + +export const createPayloadItemsStepHighlights = [ + ["13", "payloadModuleService", "Resolve the Payload Module's service from the Medusa container."], + ["15", "createdItems", "Create items in Payload."], + ["23", "items", "Return the created items."], + ["25", "ids", "Pass the IDs of the created items to the compensation function."], + ["37", "delete", "Delete the created items from Payload if an error occurs."], +] + +```ts title="src/workflows/steps/create-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" highlights={createPayloadItemsStepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PayloadUpsertData } from "../../modules/payload/types" +import { PAYLOAD_MODULE } from "../../modules/payload" + +type StepInput = { + collection: string + items: PayloadUpsertData[] +} + +export const createPayloadItemsStep = createStep( + "create-payload-items", + async ({ items, collection }: StepInput, { container }) => { + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + const createdItems = await Promise.all( + items.map(async (item) => await payloadModuleService.create( + collection, + item + )) + ) + + return new StepResponse({ + items: createdItems.map((item) => item.doc), + }, { + ids: createdItems.map((item) => item.doc.id), + collection, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + const { ids, collection } = data + + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + await payloadModuleService.delete( + collection, + { + where: { + id: { + in: ids.join(","), + }, + }, + } + ) + } +) +``` + +You create a step with the `createStep` function. It accepts three parameters: + +1. The step's unique name. +2. An async function that receives two parameters: + - The step's input, which is an object holding the collection slug and an array of items to create in Payload. + - An object that has properties including the [Medusa container](!docs!/learn/fundamentals/medusa-container), which is a registry of Framework and commerce tools that you can access in the step. +3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution. + +In the step function, you resolve the Payload Module's service from the container. Then, you use its `create` method to create the items in Payload. + +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: + +1. The step's output, which is an object that contains the created items. +2. Data to pass to the step's compensation function. + +In the compensation function, you again resolve the Payload Module's service from the Medusa container, then delete the created items from Payload. + +### Create Payload Product Workflow + +You can now create the workflow that creates products in Payload. + +To create the workflow, create the file `src/workflows/create-payload-products.ts` with the following content: + +export const createPayloadProductsWorkflowHighlights = [ + ["12", "useQueryGraphStep", "Retrieve the products from Medusa using Query."], + ["36", "createData", "Prepare the data to create the products in Payload."], + ["66", "createPayloadItemsStep", "Create the products in Payload."], + ["81", "updateProductsWorkflow", "Update the products in Medusa with the Payload product IDs."], +] + +```ts title="src/workflows/create-payload-products.ts" badgeLabel="Medusa application" badgeColor="green" highlights={createPayloadProductsWorkflowHighlights} +import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { createPayloadItemsStep } from "./steps/create-payload-items" +import { updateProductsWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + product_ids: string[] +} + +export const createPayloadProductsWorkflow = createWorkflow( + "create-payload-products", + (input: WorkflowInput) => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "id", + "title", + "handle", + "subtitle", + "description", + "created_at", + "updated_at", + "options.*", + "variants.*", + "variants.options.*", + "thumbnail", + "images.*", + ], + filters: { + id: input.product_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const createData = transform({ + products, + }, (data) => { + return { + collection: "products", + items: data.products.map((product) => ({ + medusa_id: product.id, + createdAt: product.created_at as string, + updatedAt: product.updated_at as string, + title: product.title, + handle: product.handle, + subtitle: product.subtitle, + description: product.description || "", + options: product.options.map((option) => ({ + title: option.title, + medusa_id: option.id, + })), + variants: product.variants.map((variant) => ({ + title: variant.title, + medusa_id: variant.id, + option_values: variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: option.value, + })), + })), + })), + } + }) + + const { items } = createPayloadItemsStep( + createData + ) + + const updateData = transform({ + items, + }, (data) => { + return data.items.map((item) => ({ + id: item.medusa_id, + metadata: { + payload_id: item.id, + }, + })) + }) + + updateProductsWorkflow.runAsStep({ + input: { + products: updateData, + }, + }) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter. + +It accepts a second parameter: a constructor function that holds the workflow's implementation. The function accepts an input object holding the IDs of the products to create in Payload. + +In the workflow, you: + +1. Retrieve the products from Medusa using the `useQueryGraphStep`. + - This step uses [Query](!docs!/learn/fundamentals/module-links/query) to retrieve data across modules. +2. Prepare the data to create the products in Payload. + - To manipulate data in a workflow, you need to use the `transform` function. Learn more in the [Data Manipulation](!docs!/learn/fundamentals/workflows/variable-manipulation) documentation. +3. Create the products in Payload using the `createPayloadItemsStep` you created earlier. +4. Prepare the data to update the products in Medusa with the Payload product IDs. + - You store the payload ID in the `metadata` field of the Medusa product. +5. Update the products in Medusa using the `updateProductsWorkflow`. + +A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor. + +You'll use this workflow in the next steps to create Medusa products in Payload. + +--- + +## Step 6: Trigger Product Creation in Payload + +In this step, you'll allow Medusa Admin users to trigger the creation of Medusa products in Payload. To implement this, you'll create: + +- An [API route](!docs!/learn/fundamentals/api-routes) that emits a `products.sync-payload` event. +- A [subscriber](!docs!/learn/fundamentals/events-and-subscribers) that listens to the `products.sync-payload` event and executes the `createPayloadProductsWorkflow`. +- A [setting page](!docs!/learn/fundamentals/admin/ui-routes) in the Medusa Admin that allows admin users to trigger the product creation in Payload. + +### a. Trigger Product Sync API Route + +An API route is a REST endpoint that exposes functionalities to clients, such as storefronts and the Medusa Admin. + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`. + + + +Refer to the [API routes](!docs!/learn/fundamentals/api-routes) to learn more about them. + + + +Create the file `src/api/admin/payload/sync/[collection]/route.ts` with the following content: + +```ts title="src/api/admin/payload/sync/[collection]/route.ts" badgeLabel="Medusa application" badgeColor="green" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { collection } = req.params + const eventModuleService = req.scope.resolve("event_bus") + + await eventModuleService.emit({ + name: `${collection}.sync-payload`, + data: {}, + }) + + return res.status(200).json({ + message: `Syncing ${collection} with Payload`, + }) +} +``` + +Since you export a `POST` route handler function, you're exposing a `POST` API route at `/admin/payload/sync/[collection]`, where `[collection]` is a path parameter that represents the collection slug in Payload. + +In the function, you resolve the [Event Module](../../../infrastructure-modules/event/page.mdx)'s service and emit a `{collection}.sync-payload` event, where `{collection}` is the collection slug passed in the request. + +Finally, you return a success response with a message indicating that the collection is being synced with Payload. + +### b. Create Subscriber for the Event + +Next, you'll create a subscriber that listens to the `products.sync-payload` event and executes the `createPayloadProductsWorkflow`. + +A subscriber is an asynchronous function that is executed whenever its associated event is emitted. + + + +Refer to the [Subscribers documentation](!docs!/learn/fundamentals/events-and-subscribers) to learn more about subscribers. + + + +To create a subscriber, create the file `src/subscribers/products-sync-payload.ts` with the following content: + +export const productsSyncPayloadSubscriberHighlights = [ + ["15", "products", "Retrieve paginated products from Medusa."], + ["31", "filteredProducts", "Filter out the products that are already linked to Payload."], + ["37", "createPayloadProductsWorkflow", "Execute the workflow to create products in Payload."] +] + +```ts title="src/subscribers/products-sync-payload.ts" badgeLabel="Medusa application" badgeColor="green" highlights={productsSyncPayloadSubscriberHighlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductsWorkflow } from "../workflows/create-payload-products" + +export default async function productSyncPayloadHandler({ + container, +}: SubscriberArgs) { + const query = container.resolve("query") + + const limit = 1000 + let offset = 0 + let count = 0 + + do { + const { + data: products, + metadata: { count: totalCount } = {}, + } = await query.graph({ + entity: "product", + fields: [ + "id", + "metadata", + ], + pagination: { + take: limit, + skip: offset, + }, + }) + + count = totalCount || 0 + offset += limit + const filteredProducts = products.filter((product) => !product.metadata?.payload_id) + + if (filteredProducts.length === 0) { + break + } + + await createPayloadProductsWorkflow(container) + .run({ + input: { + product_ids: filteredProducts.map((product) => product.id), + }, + }) + + } while (count > offset + limit) +} + +export const config: SubscriberConfig = { + event: "products.sync-payload", +} +``` + +A subscriber file must export: + +- An asynchronous function, which is the subscriber function that is executed when the event is emitted. +- A configuration object that defines the event the subscriber listens to. + +In the subscriber, you use [Query](!docs!/learn/fundamentals/module-links/query) to retrieve all products from Medusa. + +Then, you filter the products to only include those that don't have a payload product ID set in `product.metadata.payload_id`, and you execute the `createPayloadProductsWorkflow` with the filtered products' IDs. + +Whenever the `products.sync-payload` event is emitted, the subscriber will be executed, which will create the products in Payload. + +### c. Create Setting Page in Medusa Admin + +Next, you'll create a setting page in the Medusa Admin that allows admin users to trigger syncing products with Payload. + +#### Initialize JS SDK + +To send requests from your Medusa Admin customizations to the Medusa server, you need to initialize the [JS SDK](../../../js-sdk/page.mdx). + +Create the file `src/admin/lib/sdk.ts` with the following content: + +```ts title="src/admin/lib/sdk.ts" badgeLabel="Medusa application" badgeColor="green" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +Refer to the [JS SDK documentation](../../../js-sdk/page.mdx) to learn more about initializing the SDK. + +#### Create the Setting Page + +A setting page is a [UI route](!docs!/learn/fundamentals/admin/ui-routes) that adds a custom page to the Medusa Admin under the Settings section. The UI route is a React component that renders the page's content. + + + +Refer to the [UI Routes](!docs!/learn/fundamentals/admin/ui-routes) documentation to learn more. + + + +To create the setting page, create the file `src/admin/routes/settings/payload/page.tsx` with the following content: + +```tsx title="src/admin/routes/settings/payload/page.tsx" badgeLabel="Medusa application" badgeColor="green" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Button, Container, Heading, toast } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../../../lib/sdk" + +const PayloadSettingsPage = () => { + const { + mutateAsync: syncProductsToPayload, + isPending: isSyncingProductsToPayload, + } = useMutation({ + mutationFn: (collection: string) => + sdk.client.fetch(`/admin/payload/sync/${collection}`, { + method: "POST", + }), + onSuccess: () => toast.success(`Triggered syncing collection data with Payload`), + }) + + return ( + +
+ Payload Settings +
+
+

+ This page allows you to trigger syncing your Medusa data with Payload. It + will only create items not in Payload. +

+ +
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Payload", +}) + +export default PayloadSettingsPage +``` + +A settings page file must export: + +1. A React component that renders the page. This is the file's default export. +2. A configuration object created with the `defineRouteConfig` function. It accepts an object with properties that define the page's configuration, such as its sidebar label. + +In the page's component, you define a mutation function using Tanstack Query and the JS SDK. This function will send a `POST` request to the API route you created earlier to trigger syncing products with Payload. + +Then, you render a button that, when clicked, calls the mutation function to trigger the syncing process. + +### d. Test Product Syncing + +You can now test syncing products from Medusa to Payload. To do that: + +1. Start your Medusa application with the following command: + +```bash npm2yarn badgeLabel="Medusa application" badgeColor="green" +npm run dev +``` + +2. Run the Next.js Starter Storefront with the command: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +3. Open the Medusa Admin at `localhost:9000/app` and log in with your admin user. +4. Go to Settings -> Payload. +5. On the setting page, click the "Sync Products to Payload" button. + +![The Payload Settings page with the Sync Products button](https://res.cloudinary.com/dza7lstvk/image/upload/v1754486648/Medusa%20Resources/CleanShot_2025-08-06_at_16.23.45_2x_ubug2y.png) + +You'll see a success message indicating that the products are being synced with Payload. You can also confirm that the event was triggered by checking the Medusa server logs for the following message: + +```bash +info: Processing products.sync-payload which has 1 subscribers +``` + +To check that the products were created in Payload, open the Payload admin at `localhost:8000/admin` and go to "Products" from the sidebar. You should see your Medusa products listed there. + +![The Products collection in Payload with Medusa products](https://res.cloudinary.com/dza7lstvk/image/upload/v1754486747/Medusa%20Resources/CleanShot_2025-08-06_at_16.25.31_2x_h874oa.png) + +If you click on a product, you can edit its details, such as its title or description. + +--- + +## Step 7: Automatically Create Product in Payload + +In this step, you'll handle the `product.created` event to automatically create a product in Payload whenever a product is created in Medusa. + +You already have the workflow to create a product in Payload, so you only need to create a subscriber that listens to the `product.created` event and executes the `createPayloadProductsWorkflow`. + +Create the file `src/subscribers/product-created.ts` with the following content: + +```ts title="src/subscribers/product-created.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductsWorkflow } from "../workflows/create-payload-products" + +export default async function productCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await createPayloadProductsWorkflow(container) + .run({ + input: { + product_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +This subscriber listens to the `product.created` event and executes the `createPayloadProductsWorkflow` with the created product's ID. + +### Test Automatic Product Creation + +To test out the automatic product creation in Payload, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, create a product in Medusa using the Medusa Admin. If you check the Products collection in the Payload admin, you should see the newly created product there as well. + +--- + +## Step 8: Customize Storefront to Display Payload Products + +Now that you've integrated Payload with Medusa, you can customize the Next.js Starter Storefront to display product content from Payload. By doing so, you can show product content and assets that are optimized for the storefront. + +In this step, you'll customize the Next.js Starter Storefront to view the product title, description, images, and option values from Payload. + +### a. Fetch Payload Data with Product Data + +When you fetch product data in the Next.js Starter Storefront from the Medusa server, you can also retrieve the linked product data from Payload. + +To do this, go to `src/lib/products.ts` in your Next.js Starter Storefront. You'll find a `listProducts` function that uses the JS SDK to fetch products from the Medusa server. + +Find the `sdk.client.fetch` call and add `*payload_product` to the `fields` query parameter: + +```ts title="src/lib/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["19"]]} +export const listProducts = async ({ + // ... +}: { + //... +}): Promise<{ + // ... +}> => { + // ... + return sdk.client + .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>( + `/store/products`, + { + method: "GET", + query: { + limit, + offset, + region_id: region?.id, + fields: + "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*payload_product", + ...queryParams, + }, + headers, + next, + cache: "force-cache", + } + ) + // ... +} +``` + +Passing this field is possible because you [defined the virtual read-only link](#step-4-create-virtual-read-only-link-to-products) between the `Product` model in Medusa and the `Products` collection in Payload. + +Medusa will now return the payload data of a product from Payload and include it in the `payload_product` field of the product object. + +### b. Define Payload Product Type + +Next, you'll define a TypeScript type that adds the `payload_product` property to Medusa's `StoreProduct` type. + +In `src/types/global.ts`, add the following imports at the top of the file: + +```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue" +import { StoreProduct } from "@medusajs/types" +// @ts-ignore +import type { SerializedEditorState } from "@payloadcms/richtext-lexical/lexical" +``` + +Then, add the following type definition at the end of the file: + +```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue" +export type StoreProductWithPayload = StoreProduct & { + payload_product?: { + medusa_id: string + title: string + handle: string + subtitle?: string + description?: SerializedEditorState + thumbnail?: { + id: string + url: string + } + images: { + id: string + image: { + id: string + url: string + } + }[] + options: { + medusa_id: string + title: string + }[] + variants: { + medusa_id: string + title: string + option_values: { + medusa_option_id: string + value: string + }[] + }[] + } +} +``` + +The `StoreProductWithPayload` type extends the `StoreProduct` type from Medusa and adds the `payload_product` property. This property contains the product data from Payload, including its title, description, images, options, and variants. + +### c. Display Payload Product Title and Description + +Next, you'll customize the product details page to display the product title and description from Payload. + +To do that, you need to customize the `ProductInfo` component in `src/modules/products/templates/product-info/index.tsx`. + +First, add the following import statement at the top of the file: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../../types/global" +// @ts-ignore +import { RichText } from "@payloadcms/richtext-lexical/react" +``` + +Then, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductInfoProps = { + product: StoreProductWithPayload +} +``` + +Next, find in the `ProductInfo` component's `return` statement where the product title is displayed and replace it with the following: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"]]} +return ( +
+
+ {/* ... */} + + {product?.payload_product?.title || product.title} + + {/* ... */} +
+
+) +``` + +Also, find where the product description is displayed and replace it with the following: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} +return ( +
+
+ {/* ... */} + {product?.payload_product?.description !== undefined && + + } + + {product?.payload_product?.description === undefined && ( + + {product.description} + + )} + {/* ... */} +
+
+) +``` + +If the product has a description in Payload, it will be displayed using Payload's `RichText` component, which renders the rich text content. Otherwise, it will display the product description from Medusa. + +### d. Display Payload Product Images + +Next, you'll display the product images from Payload in the product details page and in the product preview component that is shown in the product list. + +#### Add Image Utility Functions + +You'll first create utility functions useful for retrieving the images of a product. + +Create the file `src/lib/util/payload-images.ts` with the following content: + +```ts title="src/lib/util/payload-images.ts" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../types/global" + +export function getProductImages(product: StoreProductWithPayload) { + return product?.payload_product?.images?.map((image) => ({ + id: image.id, + url: formatPayloadImageUrl(image.image.url), + })) || product.images || [] +} + +export function formatPayloadImageUrl(url: string): string { + return url.replace(/^\/api\/media\/file/, "") +} +``` + +You define two functions: + +- `getProductImages`: This function accepts a product and returns either the images from Payload or the images from Medusa if the product doesn't have images in Payload. +- `formatPayloadImageUrl`: This function formats the image URL from Payload by removing the `/api/media/file` prefix, which is not needed for displaying the image in the storefront. + +#### Update ImageGallery Props + +Next, you'll update the type of the `ImageGallery` component's props to receive an array of objects rather than an array of Medusa images. This ensures the component can accept images from Payload. + +In `src/modules/products/components/image-gallery/index.tsx`, update the `ImageGalleryProps` type to the following: + +```tsx title="src/modules/products/components/image-gallery/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ImageGalleryProps = { + images: { + id: string + url: string + }[] +} +``` + +The `ImageGallery` component can now accept an array of image objects, each with an `id` and a `url`. + +#### Display Images in Product Details Page + +To display the product images in the product details page, add the following imports at the top of `src/modules/products/templates/index.tsx`: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../types/global" +import { getProductImages } from "../../../lib/util/payload-images" +``` + +Next, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductTemplateProps = { + product: StoreProductWithPayload + // ... +} +``` + +Then, add the following before the `ProductTemplate` component's `return` statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["7"]]} +const ProductTemplate: React.FC = ({ + product, + region, + countryCode, +}) => { + // ... + const productImages = getProductImages(product) + // ... +} +``` + +You retrieve the images to display using the `getProductImages` function you created earlier. + +Finally, update the `images` prop of the `ImageGallery` component in the `return` statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]} +return ( + <> + {/* ... */} + + {/* ... */} + +) +``` + +The images on the product's details page will now be the images from Payload if available, or the images from Medusa if not. + +#### Display Images in Product Preview + +To display the product images in the product preview component that is displayed in the product list, add the following imports at the top of `src/modules/products/components/product-preview/index.tsx`: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../../types/global" +import { formatPayloadImageUrl, getProductImages } from "../../../../lib/util/payload-images" +``` + +Then, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]} +export default async function ProductPreview({ + product, + // ... +}: { + product: StoreProductWithPayload + // ... +}) { + // ... +} +``` + +Next, add the following before the `return` statement: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["8"]]} +export default async function ProductPreview({ + // ... +}: { + // ... +}) { + // ... + + const productImages = getProductImages(product) + + // ... +} +``` + +You retrieve the images to display using the `getProductImages` function you created earlier. + +After that, update the `thumbnail` and `images` props of the `Thumbnail` component in the `return` statement: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"]]} +return ( + + {/* ... */} + + {/* ... */} + +) +``` + +The thumbnail shown in the product listing will now use the thumbnail from Payload if available, or the thumbnail from Medusa if not. + +You'll also display the product title from Payload in the product preview. Find the following lines in the `return` statement: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]} +return ( + + {/* ... */} + + {product.title} + + {/* ... */} + +) +``` + +And replace them with the following: + +```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]} +return ( + + {/* ... */} + + {product.payload_product?.title || product.title} + + {/* ... */} + +) +``` + +The product title in the product preview will now be the title from Payload if available, or the title from Medusa if not. + +### e. Display Product Options and Values + +The last change you'll make is to display the title of product options and their values from Payload in the product details page. + +In `src/modules/products/components/product-actions/index.tsx`, add the following import at the top of the file: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { StoreProductWithPayload } from "../../../../types/global" +``` + +Then, change the type of the `product` prop to `StoreProductWithPayload`: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductActionsProps = { + product: StoreProductWithPayload + // ... +} +``` + +Next, find the `optionsAsKeymap` function and replace it with the following: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const optionsAsKeymap = ( + variantOptions: HttpTypes.StoreProductVariant["options"], + payloadData: StoreProductWithPayload["payload_product"] +) => { + const firstVariant = payloadData?.variants?.[0] + return variantOptions?.reduce((acc: Record, varopt: any) => { + acc[varopt.option_id] = firstVariant?.option_values.find( + (v) => v.medusa_option_id === varopt.id + )?.value || varopt.value + return acc + }, {}) +} +``` + +You update the function to receive a `payloadData` parameter, which is the product data from Payload. This allows you to retrieve the option values from Payload instead of Medusa. + +Then, in the `ProductActions` component, update all usages of the `optionsAsKeymap` function to pass the `product.payload_product` data: + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]} +// update all usages of optionsAsKeymap +const variantOptions = optionsAsKeymap( + product.variants[0].options, + product.payload_product +) +``` + +Finally, in the `return` statement, find the loop over `product.options` and replace it with the following: + +export const productActionsComponentHighlights = [ + ["5", "payloadOption", "Retrieve the Payload option data if available."], + ["14", "title", "Use the Payload option title if available, otherwise use the Medusa option title."] +] + +```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={productActionsComponentHighlights} +return ( + <> + {/* ... */} + {(product.options || []).map((option) => { + const payloadOption = product.payload_product?.options?.find( + (o) => o.medusa_id === option.id + ) + return ( +
+ +
+ ) + })} + {/* ... */} + +) +``` + +You change the `title` prop of the `OptionSelect` component to use the title from Payload if available, or the Medusa option title if not. + +Now, the product options and values will be displayed using the data from Payload, if available. + +### Test Storefront Customization + +To test out the storefront customization, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the storefront at `localhost:8000` and click on Menu -> Store. In the products listing page, you'll see thumbnails and titles of the products from Payload. + +![Product listing page in the storefront showing details from Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754491849/Medusa%20Resources/CleanShot_2025-08-06_at_17.50.34_2x_pj0hjs.png) + +If you click on a product, you'll see the product details page with the product title, description, images, and options from Payload. + +![Product details page in the storefront showing details from Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754491898/Medusa%20Resources/CleanShot_2025-08-06_at_17.51.28_2x_eokeyg.png) + +--- + +## Step 9: Handle Medusa Product Events + +In this step, you'll create subscribers and workflows to handle the following Medusa product events: + +- [product.deleted](#a-handle-product-deletions): Delete the product in Payload when a product is deleted in Medusa. +- [product-variant.created](#b-handle-product-variant-creation): Add a product variant to a product in Payload when a product variant is created in Medusa. +- [product-variant.updated](#c-handle-product-variant-updates): Update a product variant's option values in Payload when a product variant is updated in Medusa. +- [product-variant.deleted](#d-handle-product-variant-deletions): Remove a product's variant in Payload when a product variant is deleted in Medusa. +- [product-option.created](#e-handle-product-option-creation): Add a product option to a product in Payload when a product option is created in Medusa. +- [product-option.deleted](#f-handle-product-option-deletions): Remove a product's option in Payload when a product option is deleted in Medusa. + +### a. Handle Product Deletions + +To handle the `product.deleted` event, you'll create a workflow that deletes the product from Payload, then create a subscriber that executes the workflow when the event is emitted. + +The workflow will have the following steps: + + + +#### deletePayloadItemsStep + +First, you need to create the `deletePayloadItemsStep` that allows you to delete items from a Payload collection. + +Create the file `src/workflows/steps/delete-payload-items.ts` with the following content: + +```ts title="src/workflows/steps/delete-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PAYLOAD_MODULE } from "../../modules/payload" + +type StepInput = { + collection: string; + where: Record; +} + +export const deletePayloadItemsStep = createStep( + "delete-payload-items", + async ({ where, collection }: StepInput, { container }) => { + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + const prevData = await payloadModuleService.find(collection, { + where, + }) + + await payloadModuleService.delete(collection, { + where, + }) + + return new StepResponse({}, { + prevData, + collection, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + const { prevData, collection } = data + + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + for (const item of prevData.docs) { + await payloadModuleService.create( + collection, + item + ) + } + } +) +``` + +This step accepts a collection slug and a `where` condition to specify which items to delete from Payload. + +In the step, you first retrieve the existing items that match the `where` condition using the `find` method in the Payload Module's service. You pass these items to the compensation function so that you can restore them if an error occurs in the workflow. + +Then, you delete the items using the `delete` method of the Payload Module's service. + +#### Delete Payload Products Workflow + +Next, to create the workflow that deletes products from Payload, create the file `src/workflows/delete-payload-products.ts` with the following content: + +```ts title="src/workflows/delete-payload-products.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { deletePayloadItemsStep } from "./steps/delete-payload-items" + +type WorkflowInput = { + product_ids: string[] +} + +export const deletePayloadProductsWorkflow = createWorkflow( + "delete-payload-products", + ({ product_ids }: WorkflowInput) => { + const deleteProductsData = transform({ + product_ids, + }, (data) => { + return { + collection: "products", + where: { + medusa_id: { + in: data.product_ids.join(","), + }, + }, + } + }) + + deletePayloadItemsStep(deleteProductsData) + + return new WorkflowResponse(void 0) + } +) +``` + +This workflow receives the IDs of the products to delete from Payload. + +In the workflow, you prepare the data to delete from Payload using the `transform` function, then call the `deletePayloadItemsStep` to delete the products from Payload where the `medusa_id` matches one of the provided product IDs. + +#### Product Deleted Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product.deleted` event is emitted. + +Create the file `src/subscribers/product-deleted.ts` with the following content: + +```ts title="src/subscribers/product-deleted.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deletePayloadProductsWorkflow } from "../workflows/delete-payload-products" + +export default async function productDeletedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await deletePayloadProductsWorkflow(container) + .run({ + input: { + product_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.deleted", +} +``` + +This subscriber listens to the `product.deleted` event and executes the `deletePayloadProductsWorkflow` with the deleted product's ID. + +#### Test Product Deletion Handling + +To test the product deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and go to the products list. Delete a product that exists in Payload. + +If you check the Products collection in Payload, you should see that the product has been removed from there as well. + +### b. Handle Product Variant Creation + +To handle the `product-variant.created` event, you'll create a workflow that adds the new variant to the corresponding product in Payload. + +The workflow will have the following steps: + + + +#### Create Payload Product Variant Workflow + +Create the file `src/workflows/create-payload-product-variant.ts` with the following content: + +```ts title="src/workflows/create-payload-product-variant.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types" +import { updatePayloadItemsStep } from "./steps/update-payload-items" + +type WorkflowInput = { + variant_ids: string[]; +} + +export const createPayloadProductVariantWorkflow = createWorkflow( + "create-payload-product-variant", + ({ variant_ids }: WorkflowInput) => { + const { data: productVariants } = useQueryGraphStep({ + entity: "product_variant", + fields: [ + "id", + "title", + "options.*", + "options.option.*", + "product.payload_product.*", + ], + filters: { + id: variant_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const updateData = transform({ + productVariants, + }, (data) => { + const items: Record = {} + + data.productVariants.forEach((variant) => { + // @ts-expect-error + const payloadProduct = variant.product?.payload_product as PayloadCollectionItem + if (!payloadProduct) {return} + if (!items[payloadProduct.id]) { + items[payloadProduct.id] = { + variants: payloadProduct.variants || [], + } + } + + items[payloadProduct.id].variants.push({ + title: variant.title, + medusa_id: variant.id, + option_values: variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: option.value, + })), + }) + }) + + return { + collection: "products", + items: Object.keys(items).map((id) => ({ + id, + ...items[id], + })), + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product variants to add to Payload. + +In the workflow, you: + +1. Retrieve the product variant details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload. +2. Prepare the data to update the product in Payload by adding the new variant to the existing variants array. +3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Variant Created Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-variant.created` event is emitted. + +Create the file `src/subscribers/variant-created.ts` with the following content: + +```ts title="src/subscribers/variant-created.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductVariantWorkflow } from "../workflows/create-payload-product-variant" + +export default async function productVariantCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await createPayloadProductVariantWorkflow(container) + .run({ + input: { + variant_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-variant.created", +} +``` + +This subscriber listens to the `product-variant.created` event and executes the `createPayloadProductVariantWorkflow` with the created variant's ID. + +#### Test Product Variant Creation Handling + +To test the product variant creation handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Add a new variant to the product and save the changes. + +If you check the product in Payload, you should see that the new variant has been added to the product's variants array. + +### c. Handle Product Variant Updates + +To handle the `product-variant.updated` event, you'll create a workflow that updates the variant in the corresponding product in Payload. + +The workflow will have the following steps: + + + +#### Update Payload Product Variants Workflow + +Since you already have the necessary steps, you only need to create the workflow that uses these steps. + +Create the file `src/workflows/update-payload-product-variants.ts` with the following content: + +```ts title="src/workflows/update-payload-product-variants.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types" +import { updatePayloadItemsStep } from "./steps/update-payload-items" + +type WorkflowInput = { + variant_ids: string[]; +} + +export const updatePayloadProductVariantsWorkflow = createWorkflow( + "update-payload-product-variants", + ({ variant_ids }: WorkflowInput) => { + const { data: productVariants } = useQueryGraphStep({ + entity: "product_variant", + fields: [ + "id", + "title", + "options.*", + "options.option.*", + "product.payload_product.*", + ], + filters: { + id: variant_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const updateData = transform({ + productVariants, + }, (data) => { + const items: Record = {} + + data.productVariants.forEach((variant) => { + // @ts-expect-error + const payloadProduct = variant.product?.payload_product as PayloadCollectionItem + if (!payloadProduct) {return} + + if (!items[payloadProduct.id]) { + items[payloadProduct.id] = { + variants: payloadProduct.variants || [], + } + } + + // Find and update the existing variant in the payload product + const existingVariantIndex = items[payloadProduct.id].variants.findIndex( + (v: any) => v.medusa_id === variant.id + ) + + if (existingVariantIndex >= 0) { + // check if option values need to be updated + const existingVariant = items[payloadProduct.id].variants[existingVariantIndex] + const updatedOptionValues = variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: existingVariant.option_values.find((ov: any) => ov.medusa_id === option.id)?.value || + option.value, + })) + + items[payloadProduct.id].variants[existingVariantIndex] = { + ...existingVariant, + option_values: updatedOptionValues, + } + } else { + // Add the new variant to the payload product + items[payloadProduct.id].variants.push({ + title: variant.title, + medusa_id: variant.id, + option_values: variant.options.map((option) => ({ + medusa_id: option.id, + medusa_option_id: option.option?.id, + value: option.value, + })), + }) + } + }) + + return { + collection: "products", + items: Object.keys(items).map((id) => ({ + id, + ...items[id], + })), + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product variants to update in Payload. + +In the workflow, you: + +1. Retrieve the product variant details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload. +2. Prepare the data to update the product in Payload by finding and updating the existing variant in the variants array. You only update the variant's option values, in case a new one is added. +3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Variant Updated Subscriber + +Finally, you'll create the subscriber that executes the workflow. + +Create the file `src/subscribers/variant-updated.ts` with the following content: + +```ts title="src/subscribers/variant-updated.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { updatePayloadProductVariantsWorkflow } from "../workflows/update-payload-product-variants" + +export default async function productVariantUpdatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await updatePayloadProductVariantsWorkflow(container) + .run({ + input: { + variant_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-variant.updated", +} +``` + +This subscriber listens to the `product-variant.updated` event and executes the `updatePayloadProductVariantsWorkflow` with the updated variant's ID. + +#### Test Product Variant Update Handling + +To test the product variant update handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Edit an existing variant's title and save the changes. + +If you check the product in Payload, you should see that the variant's option values have been updated in the product's variants array. + +### d. Handle Product Variant Deletions + +To handle the `product-variant.deleted` event, you'll create a workflow that removes the variant from the corresponding product in Payload. + +The workflow will have the following steps: + + + +#### retrievePayloadItemsStep + +Since the `deletePayloadProductVariantsWorkflow` is executed after a product variant is deleted, you can't retrieve the product variant data from Medusa. + +Instead, you'll create a step that retrieves the products containing the variants from Payload. You'll then use this data to update the products in Payload. + +To create the step, create the file `src/workflows/steps/retrieve-payload-items.ts` with the following content: + +```ts title="src/workflows/steps/retrieve-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PAYLOAD_MODULE } from "../../modules/payload" + +type StepInput = { + collection: string; + where: Record; +} + +export const retrievePayloadItemsStep = createStep( + "retrieve-payload-items", + async ({ where, collection }: StepInput, { container }) => { + const payloadModuleService = container.resolve(PAYLOAD_MODULE) + + const items = await payloadModuleService.find(collection, { + where, + }) + + return new StepResponse({ + items: items.docs, + }) + } +) +``` + +This step accepts a collection slug and a `where` condition to specify which items to retrieve from Payload, then returns the found items. + +#### Delete Payload Product Variants Workflow + +To create the workflow, create the file `src/workflows/delete-payload-product-variants.ts` with the following content: + +```ts title="src/workflows/delete-payload-product-variants.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { updatePayloadItemsStep } from "./steps/update-payload-items" +import { retrievePayloadItemsStep } from "./steps/retrieve-payload-items" + +type WorkflowInput = { + variant_ids: string[] +} + +export const deletePayloadProductVariantsWorkflow = createWorkflow( + "delete-payload-product-variants", + ({ variant_ids }: WorkflowInput) => { + const retrieveData = transform({ + variant_ids, + }, (data) => { + return { + collection: "products", + where: { + "variants.medusa_id": { + in: data.variant_ids.join(","), + }, + }, + } + }) + + const { items: payloadProducts } = retrievePayloadItemsStep(retrieveData) + + const updateData = transform({ + payloadProducts, + variant_ids, + }, (data) => { + const items = data.payloadProducts.map((payloadProduct) => ({ + id: payloadProduct.id, + variants: payloadProduct.variants.filter((v: any) => !data.variant_ids.includes(v.medusa_id)), + })) + + return { + collection: "products", + items, + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + // Call the step to update the payload items + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product variants to delete from Payload. + +In the workflow, you: + +1. Retrieve the Payload data of the products that the variants belong to using `retrievePayloadItemsStep`. +2. Prepare the data to update the products in Payload by filtering out the variants that should be deleted. +3. Update the products in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Variant Deleted Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-variant.deleted` event is emitted. + +Create the file `src/subscribers/variant-deleted.ts` with the following content: + +```ts title="src/subscribers/variant-deleted.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deletePayloadProductVariantsWorkflow } from "../workflows/delete-payload-product-variants" + +export default async function productVariantDeletedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await deletePayloadProductVariantsWorkflow(container) + .run({ + input: { + variant_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-variant.deleted", +} +``` + +This subscriber listens to the `product-variant.deleted` event and executes the `deletePayloadProductVariantsWorkflow` with the deleted variant's ID. + +#### Test Product Variant Deletion Handling + +To test the product variant deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Delete an existing variant from the product. + +If you check the product in Payload, you should see that the variant has been removed from the product's variants array. + +### e. Handle Product Option Creation + +To handle the `product-option.created` event, you'll create a workflow that adds the new option to the corresponding product in Payload. + +The workflow will have the following steps: + + + +#### Create Payload Product Options Workflow + +You already have the necessary steps, so you only need to create the workflow that uses these steps. + +Create the file `src/workflows/create-payload-product-options.ts` with the following content: + +```ts title="src/workflows/create-payload-product-options.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types" +import { updatePayloadItemsStep } from "./steps/update-payload-items" + +type WorkflowInput = { + option_ids: string[]; +} + +export const createPayloadProductOptionsWorkflow = createWorkflow( + "create-payload-product-options", + ({ option_ids }: WorkflowInput) => { + const { data: productOptions } = useQueryGraphStep({ + entity: "product_option", + fields: [ + "id", + "title", + "product.payload_product.*", + ], + filters: { + id: option_ids, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + const updateData = transform({ + productOptions, + }, (data) => { + const items: Record = {} + + data.productOptions.forEach((option) => { + // @ts-expect-error + const payloadProduct = option.product?.payload_product as PayloadCollectionItem + if (!payloadProduct) {return} + + if (!items[payloadProduct.id]) { + items[payloadProduct.id] = { + options: payloadProduct.options || [], + } + } + + // Add the new option to the payload product + const newOption = { + title: option.title, + medusa_id: option.id, + } + + // Check if option already exists, if not add it + const existingOptionIndex = items[payloadProduct.id].options.findIndex( + (o: any) => o.medusa_id === option.id + ) + + if (existingOptionIndex === -1) { + items[payloadProduct.id].options.push(newOption) + } + }) + + return { + collection: "products", + items: Object.keys(items).map((id) => ({ + id, + ...items[id], + })), + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product options to add to Payload. + +In the workflow, you: + +1. Retrieve the product option details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload. +2. Prepare the data to update the product in Payload by adding the new option to the existing options array, checking if it doesn't already exist. +3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Option Created Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-option.created` event is emitted. + +Create the file `src/subscribers/option-created.ts` with the following content: + +```ts title="src/subscribers/option-created.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { createPayloadProductOptionsWorkflow } from "../workflows/create-payload-product-options" + +export default async function productOptionCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await createPayloadProductOptionsWorkflow(container) + .run({ + input: { + option_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-option.created", +} +``` + +This subscriber listens to the `product-option.created` event and executes the `createPayloadProductOptionsWorkflow` with the created option's ID. + +#### Test Product Option Creation Handling + +To test the product option creation handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Add a new option to the product and save the changes. + +If you check the product in Payload, you should see that the new option has been added to the product's options array. + +### f. Handle Product Option Deletions + +To handle the `product-option.deleted` event, you'll create a workflow that removes the option from the corresponding product in Payload. + +The workflow will have the following steps: + + + +#### Delete Payload Product Options Workflow + +You already have the necessary steps, so you only need to create the workflow that uses these steps. + +Create the file `src/workflows/delete-payload-product-options.ts` with the following content: + +```ts title="src/workflows/delete-payload-product-options.ts" badgeLabel="Medusa application" badgeColor="green" +import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { updatePayloadItemsStep } from "./steps/update-payload-items" +import { retrievePayloadItemsStep } from "./steps/retrieve-payload-items" + +type WorkflowInput = { + option_ids: string[] +} + +export const deletePayloadProductOptionsWorkflow = createWorkflow( + "delete-payload-product-options", + ({ option_ids }: WorkflowInput) => { + const retrieveData = transform({ + option_ids, + }, (data) => { + return { + collection: "products", + where: { + "options.medusa_id": { + in: data.option_ids.join(","), + }, + }, + } + }) + + const { items: payloadProducts } = retrievePayloadItemsStep(retrieveData) + + const updateData = transform({ + payloadProducts, + option_ids, + }, (data) => { + const items = data.payloadProducts.map((payloadProducts) => ({ + id: payloadProducts.id, + options: payloadProducts.options.filter((o: any) => !data.option_ids.includes(o.medusa_id)), + variants: payloadProducts.variants.map((variant: any) => ({ + ...variant, + option_values: variant.option_values.filter((ov: any) => !data.option_ids.includes(ov.medusa_option_id)), + })), + })) + + return { + collection: "products", + items, + } + }) + + const result = when({ updateData }, (data) => data.updateData.items.length > 0) + .then(() => { + return updatePayloadItemsStep(updateData) + }) + + const items = transform({ result }, (data) => data.result?.items || []) + + return new WorkflowResponse({ + items, + }) + } +) +``` + +This workflow receives the IDs of the product options to delete from Payload. + +In the workflow, you: + +1. Retrieve the products that contain the options to be deleted using the `retrievePayloadItemsStep`. +2. Prepare the data to update the products in Payload by filtering out the options that should be deleted. +3. Update the products in Payload using the `updatePayloadItemsStep` if there are any items to update. +4. Return the updated items from the workflow. + +#### Product Option Deleted Subscriber + +Finally, you'll create the subscriber that executes the workflow when the `product-option.deleted` event is emitted. + +Create the file `src/subscribers/option-deleted.ts` with the following content: + +```ts title="src/subscribers/option-deleted.ts" badgeLabel="Medusa application" badgeColor="green" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deletePayloadProductOptionsWorkflow } from "../workflows/delete-payload-product-options" + +export default async function productOptionDeletedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await deletePayloadProductOptionsWorkflow(container) + .run({ + input: { + option_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product-option.deleted", +} +``` + +This subscriber listens to the `product-option.deleted` event and executes the `deletePayloadProductOptionsWorkflow` with the deleted option's ID. + +#### Test Product Option Deletion Handling + +To test the product option deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running. + +Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Delete an existing option from the product. + +If you check the product in Payload, you should see that the option has been removed from the product's options array. + +--- + +## Next Steps + +You've successfully integrated Medusa with Payload to manage content related to products, variants, and options. You can expand on this integration by adding more features, such as: + +1. Managing the content of other entities, like categories or collections. The process is similar to what you've done for products: + 1. Create a collection in Payload for the entity. + 2. Create Medusa workflows and subscribers to handle the creation, update, and deletion of the entity. + 3. Display the payload data in your Next.js Starter Storefront. +2. Enable [localization](https://payloadcms.com/docs/configuration/localization) in Payload to support multiple languages. + - You only need to manage the localized content in Payload. Only the default locale will be synced with Medusa. + - You can show the localized content in your Next.js Starter Storefront based on the customer's locale. +3. Add custom fields to the Payload collections. For example, you can add images to product variants and display them in the Next.js Starter Storefront. + +### Learn More about Medusa + +If you're new to Medusa, check out the [main documentation](!docs!/learn), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](../../../commerce-modules/page.mdx). + +### Troubleshooting + +If you encounter issues during your development, check out the [troubleshooting guides](../../../troubleshooting/page.mdx). + +### Getting Help + +If you encounter issues not covered in the troubleshooting guides: + +1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions. +2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members. diff --git a/www/apps/resources/app/integrations/page.mdx b/www/apps/resources/app/integrations/page.mdx index aacc9b0e8d..01e38467e0 100644 --- a/www/apps/resources/app/integrations/page.mdx +++ b/www/apps/resources/app/integrations/page.mdx @@ -78,6 +78,14 @@ Integrate a third-party Content-Management System (CMS) to utilize rich content- children: "Tutorial" } }, + { + href: "/integrations/guides/payload", + title: "Payload CMS", + badge: { + variant: "blue", + children: "Tutorial" + } + }, { href: "/integrations/guides/sanity", title: "Sanity", @@ -88,6 +96,7 @@ Integrate a third-party Content-Management System (CMS) to utilize rich content- }, ]} className="mb-1" + itemsPerRow={2} /> --- diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 2337a5aa59..31f84eab4b 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -104,7 +104,7 @@ export const generatedEditDates = { "app/deployment/medusa-application/railway/page.mdx": "2025-04-17T08:28:58.981Z", "app/deployment/storefront/vercel/page.mdx": "2025-05-20T07:51:40.712Z", "app/deployment/page.mdx": "2025-06-24T08:50:10.114Z", - "app/integrations/page.mdx": "2025-06-25T10:48:35.928Z", + "app/integrations/page.mdx": "2025-08-07T06:06:50.604Z", "app/medusa-cli/page.mdx": "2024-08-28T11:25:32.382Z", "app/medusa-container-resources/page.mdx": "2025-07-31T13:24:15.786Z", "app/medusa-workflows-reference/page.mdx": "2025-01-20T08:21:29.962Z", @@ -6566,5 +6566,6 @@ export const generatedEditDates = { "app/commerce-modules/order/order-totals/page.mdx": "2025-07-31T15:12:10.633Z", "app/commerce-modules/user/invite-user-subscriber/page.mdx": "2025-08-01T12:01:54.551Z", "app/how-to-tutorials/tutorials/invoice-generator/page.mdx": "2025-08-04T00:00:00.000Z", + "app/integrations/guides/payload/page.mdx": "2025-08-15T07:23:50.499Z", "references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken/page.mdx": "2025-08-14T12:59:55.678Z" } \ No newline at end of file diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index 33af44de17..e5319e2cab 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -911,6 +911,10 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/integrations/guides/mailchimp/page.mdx", "pathname": "/integrations/guides/mailchimp" }, + { + "filePath": "/www/apps/resources/app/integrations/guides/payload/page.mdx", + "pathname": "/integrations/guides/payload" + }, { "filePath": "/www/apps/resources/app/integrations/guides/resend/page.mdx", "pathname": "/integrations/guides/resend" diff --git a/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs b/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs index 725cef0b0e..6fb1a4cde0 100644 --- a/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs +++ b/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs @@ -11517,6 +11517,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Integrate Payload", + "path": "https://docs.medusajs.com/resources/integrations/guides/payload", + "children": [] + }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs b/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs index 6c034ff8dd..6dc4181fd3 100644 --- a/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs +++ b/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs @@ -487,6 +487,15 @@ const generatedgeneratedHowToTutorialsSidebarSidebar = { "description": "Learn how to generate invoices for orders in your Medusa store.", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Integrate Payload CMS", + "path": "/integrations/guides/payload", + "description": "Learn how to integrate Payload CMS with Medusa to manage your product content.", + "children": [] + }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/generated/generated-integrations-sidebar.mjs b/www/apps/resources/generated/generated-integrations-sidebar.mjs index 5ffcdff216..84cddeac35 100644 --- a/www/apps/resources/generated/generated-integrations-sidebar.mjs +++ b/www/apps/resources/generated/generated-integrations-sidebar.mjs @@ -78,6 +78,14 @@ const generatedgeneratedIntegrationsSidebarSidebar = { "title": "Contentful", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/integrations/guides/payload", + "title": "Payload CMS", + "children": [] + }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/sidebars/how-to-tutorials.mjs b/www/apps/resources/sidebars/how-to-tutorials.mjs index 5196877415..26cb07e7c8 100644 --- a/www/apps/resources/sidebars/how-to-tutorials.mjs +++ b/www/apps/resources/sidebars/how-to-tutorials.mjs @@ -121,6 +121,13 @@ While tutorials show you a specific use case, they also help you understand how description: "Learn how to generate invoices for orders in your Medusa store.", }, + { + type: "ref", + title: "Integrate Payload CMS", + path: "/integrations/guides/payload", + description: + "Learn how to integrate Payload CMS with Medusa to manage your product content.", + }, { type: "link", title: "Loyalty Points System", diff --git a/www/apps/resources/sidebars/integrations.mjs b/www/apps/resources/sidebars/integrations.mjs index 07196ba6fd..fde3f61d10 100644 --- a/www/apps/resources/sidebars/integrations.mjs +++ b/www/apps/resources/sidebars/integrations.mjs @@ -52,6 +52,11 @@ export const integrationsSidebar = [ path: "/integrations/guides/contentful", title: "Contentful", }, + { + type: "link", + path: "/integrations/guides/payload", + title: "Payload CMS", + }, { type: "link", path: "/integrations/guides/sanity", diff --git a/www/packages/tags/src/tags/product.ts b/www/packages/tags/src/tags/product.ts index dc6d1df131..d73c5f0839 100644 --- a/www/packages/tags/src/tags/product.ts +++ b/www/packages/tags/src/tags/product.ts @@ -87,6 +87,10 @@ export const product = [ "title": "Localization with Contentful", "path": "https://docs.medusajs.com/resources/integrations/guides/contentful" }, + { + "title": "Integrate Payload", + "path": "https://docs.medusajs.com/resources/integrations/guides/payload" + }, { "title": "Build Wishlist Plugin", "path": "https://docs.medusajs.com/resources/plugins/guides/wishlist" diff --git a/www/packages/tags/src/tags/server.ts b/www/packages/tags/src/tags/server.ts index a7b758041d..4a3dea2232 100644 --- a/www/packages/tags/src/tags/server.ts +++ b/www/packages/tags/src/tags/server.ts @@ -119,6 +119,10 @@ export const server = [ "title": "Integrate Mailchimp", "path": "https://docs.medusajs.com/resources/integrations/guides/mailchimp" }, + { + "title": "Integrate Payload", + "path": "https://docs.medusajs.com/resources/integrations/guides/payload" + }, { "title": "Integrate Segment", "path": "https://docs.medusajs.com/resources/integrations/guides/segment" diff --git a/www/packages/tags/src/tags/tutorial.ts b/www/packages/tags/src/tags/tutorial.ts index 41998f2dd2..78cc5e6269 100644 --- a/www/packages/tags/src/tags/tutorial.ts +++ b/www/packages/tags/src/tags/tutorial.ts @@ -71,6 +71,10 @@ export const tutorial = [ "title": "Integrate Mailchimp", "path": "https://docs.medusajs.com/resources/integrations/guides/mailchimp" }, + { + "title": "Integrate Payload", + "path": "https://docs.medusajs.com/resources/integrations/guides/payload" + }, { "title": "Integrate Segment", "path": "https://docs.medusajs.com/resources/integrations/guides/segment"