Medusa

Medusa

What is Medusa?

Medusa is an ecommerce platform with a built-in framework for customization that allows you to build custom commerce applications without reinventing core commerce logic.


Installation

Reference URL

Run the following command and replace <NAME> and <DATABASE_URL> with your store name:

$ npx create-medusa-app@latest <NAME> --with-nextjs-starter --db-url "<DATABASE_URL>" --no-migrations

If you don't need any database connectivity in the beginning, you can add --skip-db, for example:

$ npx create-medusa-app@latest <NAME> --skip-db

Medusa CLI

$ npx medusa user -e <ADMIN_EMAIL> -p <ADMIN_SUPER_SECRET_PASSWORD>

Database related CLI:

$ npx medusa db:setup                      #Create the database, run migrations and sync links
$ npx medusa db:create                     #Create the database used by your application
$ npx medusa db:migrate                    #Migrate the database by executing pending migrations
$ npx medusa db:migrate:scripts            #Run all migration scripts
$ npx medusa db:rollback [modules...]      #Rollback last batch of executed migrations for a given module
$ npx medusa db:generate [modules...]      #Generate migrations for a given module

If you want to seed the database:

$ npx medusa exec ./src/scripts/seed.ts
$ medusa plugin:db:generate            #Generate migrations for modules in a plugin
$ medusa db:sync-links                 #Sync database schema with the links defined by your application and Medusa core
$ medusa plugin:build                  #Build plugin source for publishing to a package registry
$ medusa plugin:develop                #Start plugin development process in watch mode. Changes will be re-published to the local

Deployment

Railway

Backend

  1. Click on Create on right top corner
  2. Choose GitHub Repo
  3. Select or authorize the Medusa Backend repository from your own GitHub account
  4. Open the Variables tab by clicking on the newly created service
  5. Deploy the new service
NODE_ENV="production"
PORT="9000"
ADMIN_CORS="https://${{RAILWAY_PUBLIC_DOMAIN}},https://${{RAILWAY_PRIVATE_DOMAIN}}"
AUTH_CORS="https://${{RAILWAY_PUBLIC_DOMAIN}},https://${{RAILWAY_PRIVATE_DOMAIN}}"
COOKIE_SECRET=""
DATABASE_URL="postgresql://<DB_OWNER>:<DB_PASSWORD>@<NEON_INSTANCE>.<REGION>.aws.neon.tech/<DB_NAME>?sslmode=require"
JWT_SECRET=""
MEILISEARCH_HOST="https://${{MeiliSearch.MEILI_PUBLIC_URL}}"
MEILISEARCH_MASTER_KEY="${{MeiliSearch.MEILI_MASTER_KEY}}"
MINIO_ACCESS_KEY="${{Bucket.MINIO_ROOT_USER}}"
MINIO_ENDPOINT="${{Bucket.MINIO_PUBLIC_HOST}}"
MINIO_SECRET_KEY="${{Bucket.MINIO_ROOT_PASSWORD}}"
RAILWAY_HEALTHCHECK_TIMEOUT_SEC="720"
RAILWAY_PUBLIC_DOMAIN_VALUE="https://${{RAILWAY_PUBLIC_DOMAIN}}"
REDIS_URL="${{Redis.REDIS_URL}}?family=0"

S3_ACCESS_KEY_ID=""
S3_BUCKET=""
S3_ENDPOINT="https://s3.<REGION>.amazonaws.com"
S3_FILE_URL="https://<BUCKET_NAME>.s3.<REGION>.amazonaws.com"
S3_REGION="<REGION>"
S3_SECRET_ACCESS_KEY=""

SENDGRID_API_KEY=""
SENDGRID_FROM_EMAIL=""

FOURPX_API_KEY="fecfabd3-aff1-46ef-a37c-9cf125c57b39"
FOURPX_API_SECRET="71862644-c4ac-4934-95c7-71deb5601c4e"

SFEXPRESS_API_CLIENTID=""
SFEXPRESS_MODE="sandbox"
SFEXPRESS_SECRET_PRODUCTION=""
SFEXPRESS_SECRET_SANDBOX=""
STORE_CORS=""
STRIPE_API_KEY="pk_...V"
STRIPE_WEBHOOK_SECRET="whsec_..."

Storefront

  1. Click on Create on right top corner
  2. Choose GitHub Repo
  3. Select or authorize the Medusa Storefront repository from your own GitHub account
  4. Open the Variables tab by clicking on the newly created service
  5. Deploy the new service
PORT=8000
MEDUSA_BACKEND_URL=https://${{Backend.RAILWAY_PUBLIC_DOMAIN}}
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_*****************
NEXT_PUBLIC_BASE_URL=https://${{RAILWAY_PUBLIC_DOMAIN}}
NEXT_PUBLIC_DEFAULT_REGION=us
NEXT_PUBLIC_STRIPE_KEY=
REVALIDATE_SECRET=supersecret
S3_FILE_URL=<bucket_name>.s3.<region>.amazonaws.com
  1. Open the Settings tab by clicking on the newly created service
  2. Click on **Custom Domain ** under Public Networking, select the port in the dropdown menu or type `8000``` which was defined in the Variables.

Self-Hosted Medusa Backend

NODE_ENV=production

LOG_FILE=logs/medusa.log

BASE_HOST=xxxxxxxxxx.ngrok-free.app

MEDUSA_ADMIN_ONBOARDING_TYPE=default

#STORE_CORS=http://localhost:8000,https://docs.medusajs.com
STORE_CORS=/ngrok-free\.app$/
ADMIN_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com
AUTH_CORS=http://localhost:5173,http://localhost:9000,http://localhost:8000,https://docs.medusajs.com

REDIS_URL=redis://localhost:6379
#DATABASE_URL=postgresql://postgres.jvskivhrvpwsxuwwbiay:<password>@aws-1-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true

DATABASE_URL=postgres://medusa:<password>@localhost:5432/medusa


JWT_SECRET=averylongstringhere
COOKIE_SECRET=averylongstringhere

S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET=""
S3_ENDPOINT="https://s3.ap-southeast-1.amazonaws.com"
S3_FILE_URL="https://xxxxxx.s3.ap-southeast-1.amazonaws.com"
S3_REGION="ap-southeast-1"

Build for Production

Reference URL: https://docs.medusajs.com/learn/build#build-command

Option 1:
$ npx medusa build
$ cd .medusa/server && npm install
$ cp ../../.env .env.production
$ npm run start
Option 2:

Edit the package.json in the Medusa backend root folder

"scripts": {
    "build": "medusa build && ln -s .medusa/server/public/ public",
    "seed": "medusa exec ./src/scripts/seed.ts",
    "start": "medusa start",
}

Then run npm run start


Modules

Essential Modules Config

import {loadEnv, defineConfig, Modules} from '@medusajs/framework/utils'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    redisUrl: process.env.REDIS_URL,
    databaseLogging: false,
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET || "supersecret",
      cookieSecret: process.env.COOKIE_SECRET || "supersecret",
    }
  },
  modules: [
    {
      key: Modules.CACHE,
      resolve: "@medusajs/cache-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
        ttl: 30, // Time to live in seconds
      },
    },
    {
      key: Modules.EVENT_BUS,
      resolve: "@medusajs/event-bus-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
      },
    },
    {
      key: Modules.WORKFLOW_ENGINE,
      resolve: '@medusajs/workflow-engine-redis',
      options: {
        redis: {
          url: process.env.REDIS_URL,
        },
      },
    },
    {
      key: Modules.FILE,
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-s3",
            id: "s3",
            options: {
              file_url: process.env.S3_FILE_URL,
              access_key_id: process.env.S3_ACCESS_KEY_ID,
              secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
              region: process.env.S3_REGION,
              bucket: process.env.S3_BUCKET,
              endpoint: process.env.S3_ENDPOINT,
            },
          },
        ],
      },
      ...(process.env.SENDGRID_API_KEY && process.env.SENDGRID_FROM_EMAIL ? [{
        key: Modules.NOTIFICATION,
        resolve: '@medusajs/notification',
        options: {
          providers: [
            ...(process.env.SENDGRID_API_KEY && process.env.SENDGRID_FROM_EMAIL ? [{
              resolve: '@medusajs/notification-sendgrid',
              id: 'sendgrid',
              options: {
                channels: ['email'],
                api_key: process.env.SENDGRID_API_KEY,
                from: process.env.SENDGRID_FROM_EMAIL,
              }
            }] : []),
          ]
        }
      }] : []),
      ...(process.env.STRIPE_API_KEY && process.env.STRIPE_WEBHOOK_SECRET ? [{
        key: Modules.PAYMENT,
        resolve: "@medusajs/medusa/payment",
        options: {
          providers: [
            {
              resolve: "@medusajs/medusa/payment-stripe",
              id: "stripe",
              options: {
                apiKey: process.env.STRIPE_API_KEY,
              },
            },
          ],
        },
      }] : [])
    },
  ],
})

Redis

Reference URL

import {Modules} from "@medusajs/framework/utils"
// ...
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/cache-redis",
      options: {
        redisUrl: process.env.CACHE_REDIS_URL,
      },
    },
  ],
})

AWS S3 Storage

Reference URL

  • Create AWS user with AmazonS3FullAccess permissions.
  • Create AWS IAM user access key ID and secret access key.
  • Create S3 bucket with the "Public Access setting" enabled:
  1. On your bucket's dashboard, click on the Permissions tab.
  2. Click on the Edit button of the Block public access (bucket settings) section.
  3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button.
  4. Confirm saving the changes by entering confirm in the pop-up that shows.
  5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button.
  6. In the form that opens:
  • Choose the "ACLs enabled" card.
  • Click on the "Save changes" button.
  1. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button.
  2. In the form that opens, enable the Read permission for "Everyone (public access)".
  3. Check the "I understand the effects of these changes on my objects and buckets." checkbox.
  4. Click on the "Save changes" button.

Stripe Payment

Stripe Setup Prerequisites

Register or login your Stripe account. Retrieve the API key and create webhook.

Install Stripe CLI:

$ brew install stripe/stripe-cli/stripe
$ stripe login

There is a pairing code showing and a Stripe link. Open that link and verify the pairing code.

Stripe Webhook

For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments.

Webhook URL

Medusa has a {server_url}/hooks/payment/{provider_id} API route that you can use to register webhooks in Stripe, where:

  • {server_url} is the URL to your deployed Medusa application in server mode.

  • {provider_id} is the ID of the provider as explained in the Stripe Payment Provider IDs section, without the pp_ prefix.

  • The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each:

Stripe Payment TypeWebhook Endpoint URL
Basic Stripe Payment{server_url}/hooks/payment/stripe_stripe
Bancontact Payments{server_url}/hooks/payment/stripe-bancontact_stripe
BLIK Payments{server_url}/hooks/payment/stripe-blik_stripe
giropay Payments{server_url}/hooks/payment/stripe-giropay_stripe
iDEAL Payments{server_url}/hooks/payment/stripe-ideal_stripe
Przelewy24 Payments{server_url}/hooks/payment/stripe-przelewy24_stripe
PromptPay Payments{server_url}/hooks/payment/stripe-promptpay_stripe

Webhook Events

When you set up the webhook in Stripe, choose the following events to listen to:

  • payment_intent.amount_capturable_updated
  • payment_intent.succeeded
  • payment_intent.payment_failed

Backend

  1. Add it to the array of providers passed to the Payment Module in medusa-config.ts:
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
              webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
              capture: true,
              automatic_payment_methods: true,
              payment_description: "Stripe is highly secured"
            },
          },
        ],
      },
    },
  ],
})
  1. Environment Variables

Add these to your .env file:

Login your Stripe account and open the Developer Workbench Overview page. Copy the Secret key from the API keys and a Signing secret from the Webhook tab.

STRIPE_API_KEY=sk_test_your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
  1. Add Payment Provider to Region

In your Medusa admin, go to Settings > Regions and add "stripe" as a payment provider to your regions.

Storefront Stripe Implementation

Prerequisites

Steps

  1. Install Stripe SDK In your storefront, use the following command to install Stripe's JS and React SDKs:
$ pnpm add @stripe/react-stripe-js @stripe/stripe-js
  1. Add Stripe Environment Variables Next, add an environment variable holding your Stripe publishable API key. For example:
NEXT_PUBLIC_STRIPE_PK=pk_test_51Kj...

Test Payment

To test Stripe payments, use the following test credit card details:

  • Card Number: 4242 4242 4242 4242
  • Expiry Date: Any future date
  • CVC: Any 3 digits

Medusa Plugin Development

Note

Make sure there is no NODE_ENV=production set in your development environment, all devDependencies packages will not be installed. So that you will not able to run medusa cli and yalc.

You create your plugin project separately outside a Medusa backend folder.

npx create-medusa-app my-plugin --plugin

This command publishes the plugin into a local package registry (via yalc) so you can install it into your Medusa application.

npx medusa plugin:publish

After that, run the following Medusa CLI command to install the new plugin:

npx medusa plugin:add @myorg/plugin-name

If you need to remove the dev version plugin in your Medusa project, run:

yalc remove <your-plugin-name>
#or
yalc remove --all

If you need to check all packages installed by yalc, run:

yalc installations show

Register Plugin in Medusa Application (Backend)

module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {
        apiKey: true,
      },
    },
  ],
})

Watch Plugin Changes During Development

npx medusa plugin:develop

This command will:

  • Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built.
  • Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions.

Publish Plugin to NPM

npx medusa plugin:build

The command will compile an output in the .medusa/server directory.

You can now publish the plugin to npm using the NPM CLI tool. Run the following command to publish the plugin to npm:

npm publish

Install Public Plugin in Medusa Application

npm install @myorg/plugin-name

Update a Published Plugin

If you've published a plugin, and you've made changes to it, you'll have to publish the update to NPM again.

  1. First, run the following command to change the version of the plugin:
npm version <type>

Where <type> indicates the type of version update you’re publishing. For example, it can be major or minor. Refer to the npm version documentation for more information.

Then, re-run the same commands for publishing a plugin:

npx medusa plugin:build
npm publish

This will publish an updated version of your plugin under a new version.


Admin UI Routes

What is a UI Route?

The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allows admin users to perform custom actions.

For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa.

You can create a UI route directly in your Medusa application, or in a plugin if you want to share the UI route across multiple Medusa applications.

How to Create a UI Route

You create a UI route in a page.tsx file under a sub-directory of src/admin/routes directory. The file's path relative to src/admin/routes determines its path in the dashboard. The file’s default export must be the UI route’s React component.

For example, create the file src/admin/routes/custom/page.tsx with the following content:

import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export default CustomPage

You add a new route at http://localhost:9000/app/custom. The CustomPage component holds the page's content, which currently only shows a heading.

In the route, you use Medusa UI, a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it.

Show UI Route in the Sidebar

To add a sidebar item for your custom UI route, export a configuration object in the UI route's file:

import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage

The configuration object is created using defineRouteConfig from the Medusa Framework. It accepts the following properties:

  • label: the sidebar item’s label.
  • icon: an optional React component used as an icon in the sidebar.

The above example adds a new sidebar item with the label Custom Route and an icon from the Medusa UI Icons package.

Nested UI Routes

Consider that alongside the UI route above at src/admin/routes/custom/page.tsx you create a nested UI route at src/admin/routes/custom/nested/page.tsx that also exports route configurations:

import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const NestedCustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my nested custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Nested Route",
})

export default NestedCustomPage

This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked.

Caveats

Some caveats for nested UI routes in the sidebar:

  • Nested dynamic UI routes, such as one created at src/admin/routes/custom/[id]/page.tsx aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console.
  • Nested routes in settings pages aren't shown in the sidebar to follow the admin's design conventions.
  • The icon configuration is ignored for the sidebar item of nested UI routes to follow the admin's design conventions.

Route Under Existing Admin Route

You can add a custom UI route under an existing route. For example, you can add a route under the orders route: src/admin/routes/orders/nested/page.tsx

import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const NestedOrdersPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Nested Orders Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Nested Orders",
  nested: "/orders",
})

export default NestedOrdersPage

The nested property passed to defineRouteConfig specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item.

Create Settings Page

To create a page under the settings section of the admin dashboard, create a UI route under the path src/admin/routes/settings.

For example, create a UI route at src/admin/routes/settings/custom/page.tsx:

import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage

This adds a page under the path /app/settings/custom. An item is also added to the settings sidebar with the label Custom.

Path Parameters

A UI route can accept path parameters if the name of any of the directories in its path is of the format [param].

For example, create the file src/admin/routes/custom/[id]/page.tsx with the following content:

import { useParams } from "react-router-dom"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  const { id } = useParams()

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Passed ID: {id}</Heading>
      </div>
    </Container>
  )
}

export default CustomPage

You access the passed parameter using react-router-dom's useParams hook.

If you run the Medusa application and go to http://localhost:9000/app/custom/123, you'll see 123 printed in the page.

Set UI Route Breadcrumbs

The Medusa Admin dashboard shows breadcrumbs at the top of each page, if specified. This allows users to navigate through your custom UI routes.

To set the breadcrumbs of a UI route, export a handle object with a breadcrumb property in the UI route's file: src/admin/routes/custom/page.tsx

import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export default CustomPage

export const handle = {
  breadcrumb: () => "Custom Route",
}

The value of breadcrumb is a function that returns the breadcrumb label as a string, or a React JSX element.

Set Breadcrumbs for Nested UI Routes

If you set a breadcrumb for a nested UI route, and you open the route in the Medusa Admin, you'll see the breadcrumbs starting from its parent route to the nested route.

For example, if you have the following UI route at src/admin/routes/custom/nested/page.tsx that's nested under the previous one:

import { Container, Heading } from "@medusajs/ui"

const NestedCustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my nested custom route</Heading>
      </div>
    </Container>
  )
}

export default NestedCustomPage

export const handle = {
  breadcrumb: () => "Nested Custom Route",
}

Then, when you open the nested route at http://localhost:9000/app/custom/nested, you'll see the breadcrumbs as Custom Route > Nested Custom Route. Each breadcrumb is clickable, allowing users to navigate back to the parent route.

Set Breadcrumbs Dynamically

In some use cases, you may want to show a dynamic breadcrumb for a UI route. For example, if you have a UI route that displays a brand's details, you can set the breadcrumb to show the brand's name dynamically.

To do that, you can:

  1. Define and export a loader function in the UI route file that fetches the data needed for the breadcrumb.
  2. Receive the data in the breadcrumb function and return the dynamic label.

For example, create a UI route at src/admin/routes/brands/[id]/page.tsx with the following content:

import { Container, Heading } from "@medusajs/ui"
import { LoaderFunctionArgs, UIMatch, useLoaderData } from "react-router-dom"
import { sdk } from "../../../lib/sdk"

type BrandResponse = {
  brand: {
    name: string
  }
}

const BrandPage = () => {
  const { brand } = useLoaderData() as Awaited<BrandResponse>

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">{brand.name}</Heading>
      </div>
    </Container>
  )
}

export default BrandPage

export async function loader({ params }: LoaderFunctionArgs) {
  const { id } = params
  const { brand } = await sdk.client.fetch<BrandResponse>(`/admin/brands/${id}`)

  return {
    brand,
  }
}

export const handle = {
  breadcrumb: (
    { data }: UIMatch<BrandResponse>
  ) => data.brand.name || "Brand",
}

In the loader function, you retrieve the brands from a custom API route and return them.

Then, in the handle.breadcrumb function, you receive data prop containing the brand information returned by the loader function. You can use this data to return a dynamic breadcrumb label.

When you open the UI route at http://localhost:9000/app/brands/123, the breadcrumb will show the brand's name, such as Acme.

Tip: You also use the useLoaderData hook to access the data returned by the loader function in the UI route component. Learn more in the Routing Customizations chapter.


Troubleshooting

Module @medusajs/cache-redis doesn't have a serviceName. Please provide a 'key' for the module or check the service joiner config.

  1. Open the medsua-config.ts file in Medusa backend repository
import {Modules} from "@medusajs/framework/utils"
//...
modules: [
  {
    key: Modules.CACHE,
    resolve: "@medusajs/cache-redis",
    options: {
      redisUrl: process.env.REDIS_URL,
      ttl: 30, // Time to live in seconds
    },
  },
//...

Make sure the modules used have the key

Blocked request. This host example.com is not allowed. To allow this host, add example.com to server.allowedHosts in vite.config.js

Edit medusa-config.ts and add the following admin settings:

module.exports = defineConfig({
  ...,
  admin: {
    disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
    path: `/${process.env.ADMIN_PANEL_PREFIX}`,
    backendUrl: process.env.MEDUSA_BACKEND_URL,
    vite: () => ({
      server: {
        allowedHosts: ['example.com'],
      },
    }),
  },
})

OPTIMIZED_EXTERNAL_IMAGE_REQUEST_UNAUTHORIZED

DYNAMIC_SERVER_USAGE

When LOG_FILE=medusa.log commented out, then the medusa dev server can not start. It keeps running this:

info: Skipping instrumentation registration. No register function found.
{"level":"info","message":"medusa.log modified: Restarting dev server","timestamp":"2025-11-21 13:31:39"}

The dev CLI is watching the whole repo and restarts the server on any file change. It doesn’t ignore *.log, so if LOG_FILE=medusa.log is set, the Medusa logger writes to that file and every new line triggers the watcher, causing the endless “medusa.log modified: Restarting dev server” loop instead of letting the server boot (see node_modules/@medusajs/cli/dist/reporter/index.js for the file transport and node_modules/@medusajs/medusa/ dist/commands/develop.js for the chokidar watcher).

To stop the loop:

  • Make sure LOG_FILE isn’t set anywhere (shell export, env manager); remove the line from your env and restart the shell.
  • Delete any existing medusa.log that another process might still be writing to.
  • If you need a log file, point it outside the watched tree (e.g. LOG_FILE=/tmp/medusa.dev.log or LOG_FILE=.medusa/logs/medusa.log, since .medusa is ignored by the watcher).
Previous
GA4