What’s New

medusa

Frameworks & Libraries

medusa release notes.

Latest v2.18.0 · by medusaWebsitemedusajs/medusa

Changelog

v2.18.0

Highlights

This release comes with new features, bug fixes, and dependency updates for better security.

Medusa MCP users can update their project using the following prompt:

Update my Medusa project to v2.18.0
Balanced Query Load Strategy by Default

🚧 Breaking change

The default database load strategy has changed from SELECT_IN to BALANCED, matching MikroORM v7's intended default. MikroORM picks between a joined and a select-in approach per relation, which improves query performance in most cases. Because it changes how relations are loaded, review query-heavy paths and any snapshot or query-count assertions in your tests after upgrading.


Return Value of Generated Internal Service's delete Method Changed

🚧 Breaking change

This affects usages of generated internal services. They do not impact methods generated by MedusaService such as deletePosts.

The return value of the delete method of a generated internal service has been changed to cater for composite primary keys:

// before
delete(idOrSelector: string, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: string[], sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object[], sharedContext?: Context): Promise<string[]>
delete(
  idOrSelector: {
    selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
  },
  sharedContext?: Context
): Promise<string[]>

// after
delete(
  idOrSelector: string,
  sharedContext?: Context
): Promise<string[] | Record<string, any>[]>
delete(
  idOrSelector: string[],
  sharedContext?: Context
): Promise<string[] | Record<string, any>[]>
delete(
  idOrSelector: object,
  sharedContext?: Context
): Promise<string[] | Record<string, any>[]>
delete(
  idOrSelector: object[],
  sharedContext?: Context
): Promise<string[] | Record<string, any>[]>
delete(
  idOrSelector: {
    selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
  },
  sharedContext?: Context
): Promise<string[] | Record<string, any>[]>

The method now either returns an array of deleted IDs if the primary key is an id field, or an array of objects, each object's keys are the composite primary key's name, and value is the primary key's value. For example, [{ foo: "foo-1", var: "bar-1" }].

If you use generated internal services in your module, make sure to update their usages to handle the new return value. For example:

// before
const deletedIds = await postService.delete(idsOrObject) // type was string

// after
// type could be an array of string IDs or objects.
const deletedData = await postService.delete(idsOrObject)
const isStringIds = deletedData.some((data) => typeof data === "string")

Improved Configurable Data Tables in the Admin Dashboard

The view configurations feature in the Medusa Admin dashboard. has been improved Users can resize columns, control column visibility and ordering, and save view configurations, with dynamic filter and sort resolution, custom cell renderer registration, and a UI for managing property labels.

Learn more in this documentation

https://github.com/user-attachments/assets/995cdb60-f9d7-4f1b-9ce9-3b24671130fb


Transfer an Order to a Guest Customer

Orders can now be transferred to a guest customer from the admin dashboard without going through a full order transfer flow. This makes it possible to reassign an order to a guest account when it was placed under the wrong customer or needs to be detached from a registered account. The original customer transfer flow remains for transferring orders to registered customers.

#15926


New Inventory Events

The inventory workflows now emit domain events, so you can subscribe to inventory changes and run side effects such as syncing stock to external systems or notifying on low levels.

#15991


Short-Lived Database Credentials for AWS RDS IAM

createPgConnection now forwards dynamicPassword and expirationChecker from databaseDriverOptions to the underlying connection. When running on AWS RDS with IAM authentication (or any setup with short-lived credentials such as Azure AD tokens, GCP IAM, or Vault dynamic secrets), tokens expire after roughly 15 minutes. Previously these fields were silently dropped, causing authentication failures once the pool opened new connections. You can now mint a fresh token per connection:

// medusa-config.ts
import { Signer } from "@aws-sdk/rds-signer"
import { defineConfig } from "@medusajs/framework/utils"

const signer = new Signer({ hostname, port, username })

export default defineConfig({
  projectConfig: {
    databaseDriverOptions: {
      dynamicPassword: () => signer.getAuthToken(),
      expirationChecker: () => true, // always fetch a fresh token
    },
  },
})

#15686


Stripe Payment Method Configurations

The @medusajs/payment-stripe provider now supports Stripe Payment Method Configurations (PMC) through a new paymentMethodConfiguration option. The value is forwarded as payment_method_configuration when creating a PaymentIntent, and can be overridden per request via extra.payment_method_configuration. This lets merchants manage which payment methods appear to customers from the Stripe Dashboard, without code changes or redeploys.

// medusa-config.ts
{
  resolve: "@medusajs/payment-stripe",
  id: "stripe",
  options: {
    apiKey: process.env.STRIPE_API_KEY,
    paymentMethodConfiguration: "pmc_xxx",
  },
}

#15191


Security: Dependency Updates

This release resolves all Dependabot security alerts by bumping vulnerable dependencies. Notable updates include:

  • multer (2.0.2 to 2.2.0)
  • qs (6.14.0 to 6.15.2)
  • @opentelemetry/sdk-node (0.218.0 to 0.220.0)
  • @opentelemetry/resources and @opentelemetry/sdk-trace-node (2.0.0 to 2.7.0)

If you've explicitely installed any of these packages, make sure to update to the same new version to avoid unexpected issues. For the full list of updated dependencies check out PRs #16083, #16089, #16082, and 16158

Features
  • feat: Implement support for cross-module filtering at the DAL by @sradevski in #15918
  • feat: Pass crossjoinable info to joiner by @sradevski in #15951
  • feat(admin-vite-plugin,dashboard): add config.label to document title resolution seo fallback by @NicolasGorga in #15984
  • feat(dashboard,medusa,types): filter notifications in admin dashboard for logged in user by @NicolasGorga in #15923
  • feat(core-flows): automatically refresh taxes upon state change by @NicolasGorga in #15924
  • feat(dashboard): resizable datagrid columns by @NicolasGorga in #15925
  • feat(core-flows, utils): emit inventory-related events by @shahednasser in #15991
  • feat(eslint-plugin): add a rule for wildcard + specific field selections in query by @shahednasser in #16037
  • feat(core-flows,medusa,types,js-sdk,dashboard): transfer order to guest customer by @NicolasGorga in #15926
  • feat(dashboard,admin-vite-plugin,admin-shared,ui,settings,js-sdk,types): view configuration UI enhancements — dynamic filter/sort resolution, custom cell renderer registration, property labels management UI by @NicolasGorga in #14661
  • feat(framework): add helpful hint for admin users using incorrect api key header by @shahednasser in #16062
  • feat: Implement support for cross-module joins by @sradevski in #15979
  • feat: Use a balanced load strategy which will be the default in mikro… by @sradevski in #16137
  • feat(payment-stripe): add paymentMethodConfiguration option by @vssavosko in #15191
  • feat(utils): add Iranian Toman (IRT) currency by @KMLnk in #15948
  • feat(dashboard): pass order ID as a query parameter in storefront payment link by @shahednasser in #16127
  • feat: Implement second stage of cross-module joins by @sradevski in #16131
  • feat(dashboard,settings,ui,types): implement configurable data tables end to end in admin dashboard by @NicolasGorga in #16025
  • feat(create-medusa-app): detect nub package manager by @colinhacks in #15898
  • feat: Extract query engine to package by @sradevski in #15900
  • feat(core-flows,types,utils,medusa): support global product options in imports by @shahednasser in #15614
  • feat(docs): JSON doc-model TypeDoc theme + generator by @shahednasser in #16042
  • feat(docs): serve the JSON doc-model + remove MDX by @shahednasser in #16044
  • feat: Cleanup query package structure by @sradevski in #15934
  • feat: Refactor joiner by @sradevski in #15972
  • feat(dashboard): update Brazilian Portuguese (ptBR) translations by @diwberg in #16015
  • feat(utils): support dynamic password function in createPgConnection for RDS IAM auth by @mrpackethead in #15686
  • feat(types,core-flows,order): add metadata to tax lines returned by providers by @nam-stx in #15840
Bugs
  • fix(dashboard): call useStateAwareTo unconditionally in RouteFocusModal by @merkelis-p in #15752
  • fix(dashboard): fixes to sidebar styling for layout configuration by @shahednasser in #15985
  • fix(core-flows): correctly clear a region payment providers by @NicolasGorga in #15986
  • fix(eslint-plugin): remove use-query-context-utility rule by @adem-loghmari in #15850
  • fix(file-s3,file-local): decode upload content by MIME type to stop binary file corruption by @BIGSUS24 in #15811
  • fix(utils): fix delete operation when primary key is not id by @shahednasser in #15989
  • fix(utils): apply select in strategy for single entity fetches by @shahednasser in #15990
  • fix(utils): prevent nested mikroOrm serialization from mutating parent keys by @dosacha in #15806
  • fix(payment, medusa): reject refunds and captures of negative or 0 amount by @shahednasser in #16036
  • fix(medusa): use email compatible validation with zod by @shahednasser in #16055
  • fix(utils): stop double-counting tax on discounts in refundable_total for non-tax-inclusive items by @shafi-VM in #15886
  • fix(framework): fix middlewares regex matcher by @shahednasser in #16066
  • fix(test-utils): replace unmaintained pg-god with @medusajs/framework/pg by @shafi-VM in #15887
  • fix(modules-sdk, framework): fail early when can't connect to the database by @shahednasser in #16100
  • fix(framework): register HTTP compression middleware by @AkashRanjan18 in #15957
  • fix(js-sdk): handle blocked browser storage by @Arunsiva003 in #15941
  • fix(core-flows): only complete cart from payment webhook on definitive actions by @rodriguescarson in #15930
  • fix(utils): assign per-adjustment subtotal/total instead of cumulative sum by @Venkat-jaswanth in #16013
  • fix(dashboard): don't mark promotions with per-attribute campaign budgets as expired by @MahinAnowar in #16018
  • fix(dashboard): sorting countries by name or code no longer throws error by @andrewaltos in #16020
  • fix: add missing keys to Persian (fa) translation file by @rasa7x in #16071
  • fix(utils): add BigNumber.toString() to avoid '[object Object]' coercion by @Venkat-jaswanth in #16014
  • fix(dashboard): handle empty price rules on price list price updates by @MahinAnowar in #15942
  • fix(caching-redis): read ioredis options from redisOptions for consistency with other Redis modules by @mvanhorn in #16101
  • fix(core-flows): filter reservations by line item in createOrderFulfillmentWorkflow by @andrewgreenh in #16136
  • fix(framework, utils): defensive handling of dotted path segments by @shahednasser in #16123
  • fix(framework, types, medusa): add disallowed query config to restrict retrieved fields by @shahednasser in #16125
  • fix(order): version-scope shipping method adjustments in select-in (list) path by @nbwar in #15920
  • fix(create-medusa-app): fix ajv error in npm installation by @shahednasser in #16124
  • fix(product): emit PRODUCT_CATEGORY_DELETED event on soft delete by @adem-loghmari in #16126
  • fix(js-sdk): avoid Vite import-analysis conflict with Product.import method by @ashif323 in #15899
  • fix(dashboard): allow same conditional shipping price amount for different conditions by @Ligament in #16130
  • fix(dashboard): fix edit reservartion form not showing anything by @shahednasser in #16132
  • fix(payment,promotion): serialize concurrent money guards to prevent over-capture, over-refund and budget overspend by @Meliceanu in #16097
  • fix(modules-sdk): validate link uniqueness on batch links being created by @shahednasser in #16144
  • fix(core-flows): refresh cart tax lines on any tax-relevant shipping address change by @shahednasser in #16035
  • fix(dashboard): prevent product option badges from overlapping with long option values by @khchen-doit in #15902
  • fix(loyalty): honor configured admin auth type in admin SDK by @shahednasser in #16164
  • fix: Support cross module filtering on belongs to foreign key by @sradevski in #16167
  • fix(framework): load workflows from index.[js,ts] files in worker mode by @MahinAnowar in #15702
  • fix(utils): apply onUpdate hooks before nativeUpdateMany by @sawirricardo in #15694
  • fix(eslint-plugin): normalize Windows paths in module relationship rule by @gaoflow in #15784
  • fix(core-flows): pass customer groups to pricing context in createCartWorkflow by @Kelpy2004 in #15746
  • fix(admin,utils): add AOA currency and guard region editor against unknown currencies by @BIGSUS24 in #15797
  • fix(core-flows): translate line items when creating an order with a l… by @shafi-VM in #15804
  • fix(medusa): return 400 instead of 500 for invalid promotion rule attribute with a value filter by @shafi-VM in #15819
  • fix(rbac): add missing migration for RbacRoleInheritance model by @pevey in #15833
  • fix(medusa,utils): defineConfig typed modules by @leobenzol in #15834
  • fix(dashboard): use total count instead of page length for product type products table pagination by @Tusharkhadde in #15950
  • fix: skip self-join for readonly links in cross-module filters by @peterlgh7 in #15968
  • fix(core-flows): preserve data field in prepareTaxLinesData during cart completion by @nam-stx in #15969
  • fix(core-flows): only emit RETURN_RECEIVED when receive_now is true by @d3v07 in #15856
  • fix: resolve Dependabot security update failure for @opentelemetry/core by @shahednasser with @Copilot in #16082
Documentation
Chores
Other Changes
New Contributors
  • @MahinAnowar made their first contribution in #15702
  • @BIGSUS24 made their first contribution in #15796
  • @Kelpy2004 made their first contribution in #15746
  • @nam-stx made their first contribution in #15840
  • @d3v07 made their first contribution in #15856
  • @dosacha made their first contribution in #15806
  • @vansh17June made their first contribution in #16058
  • @AkashRanjan18 made their first contribution in #15957
  • @Arunsiva003 made their first contribution in #15941
  • @rodriguescarson made their first contribution in #15930
  • @Venkat-jaswanth made their first contribution in #16013
  • @andrewaltos made their first contribution in #16020
  • @rasa7x made their first contribution in #16071
  • @andrewgreenh made their first contribution in #16136
  • @nbwar made their first contribution in #15920
  • @diwberg made their first contribution in #16015
  • @vssavosko made their first contribution in #15191
  • @KMLnk made their first contribution in #15948
  • @Meliceanu made their first contribution in #16097
  • @colinhacks made their first contribution in #15898
  • @khchen-doit made their first contribution in #15902

Full Changelog: v2.17.2...v2.18.0

v2.17.2

Highlights
before / after on widgets injection zones

🚧 Breaking change

Since the components in a LayoutComposer controlled page are now arranged through the Editor view (including widgets), the .after | .before widget injection zones don't have an effect on where the Widget is placed within its injection zone - you should configure this, just like with any other layout component, through the Editor view. The .side is still relevant for Two Column page layouts.

New Admin Layout Composer

The admin dashboard layout is now fully customizable through a reworked Layout Composer. You can rearrange the Topbar, Sidebar, Settings Sidebar and pages with drag and drop, and your arrangement is persisted in the database so it stays consistent across sessions and users.

This rework started as a community contribution from @leobenzol, who built the drag-and-drop Layout Composer and settings persistence and applied it across the shell. Thanks to their work, extending and reordering the dashboard layout is now a first-class part of the admin. A prime example of the kind of high-value features the community can drive in Medusa. Read more in the announcement.

#15721 #15861 #15862


Async Payment Methods Support

Medusa now supports asynchronous payment methods, where a payment is confirmed after the initial request through provider webhooks rather than synchronously. The support spans the Payment module, the Stripe provider, core workflows, the JS SDK, and the admin dashboard, so async methods are handled consistently across the checkout and payment lifecycle.

#15085


Tiered Pricing for Price List Prices

Price list prices now support quantity-based tiers directly in the admin. Click a price cell in the price list edit form to define multiple price tiers per variant and currency, making it straightforward to set up bulk discounts and other quantity-based pricing strategies across regions and currencies.

https://github.com/user-attachments/assets/1521c033-aee9-4fca-a2b8-d09ea2a95e2e

#15258


Contextual Browser Tab Titles

The admin dashboard now sets a descriptive document title for each page, so multiple open tabs are easy to tell apart. List pages show the section name (for example "Products - Medusa") and detail pages surface the entity name (for example a product's title or an order's display id). Titles are resolved through a new seo resolver on each route's handle.

Custom admin pages in your own project can set their tab title the same way, by exporting a handle with an seo resolver from the route file. Return a static title, or derive one from the route's loader data via match.data:

// src/admin/routes/custom/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"

export const handle = {
  seo: () => ({ title: "My Custom Page" }),
}

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

const CustomPage = () => {
  return <div>My custom page</div>
}

export default CustomPage

#14426

Features
  • feat: drag&drop LayoutComposer, settings db persistence by @leobenzol in #15721
  • feat(payment,payment-stripe,core-flows,medusa,dashboard,js-sdk,utils,types): introduce async payment methods support by @NicolasGorga in #15085
  • feat(dashboard): add dynamic document titles for browser tabs by @bqst in #14426
  • feat(admin-shared,dashboard): apply new LayoutComposer approach across the admin dashboard by @NicolasGorga in #15861
  • feat(admin-shared,dashboard): use layout composer in Topbar, Sidebar and settings Sidebar by @leobenzol in #15862
  • feat(dashboard): add quantity-based pricing support for price lists by @laaibaQasim in #15258
Bugs
  • fix(payment-stripe): handle async payment method check gracefully in webhook by @NicolasGorga in #15897
  • fix(core-flows): keep customer account-holder link on a failed payment-session init by @Mezzle in #15618
  • fix(utils): allow alias override in defineLink InputOptions to support linking the same module twice by @kowalski21 in #15701
  • fix(order): make custom_display_id searchable by @Dev-Abdullah-H in #15622
  • fix: Product option and product option values are not translatable by @weknowyourgame in #15881
  • fix(core-flows,medusa): exclude gift cards from tax calc and add an option for custom gift card codes by @scherddel in #15419
  • fix: move the remote joiner class to the modules SDK package by @sradevski in #15890
  • fix(order): use isDefined check for unit_price in item-update action by @Dev-Abdullah-H in #15863
Chores
New Contributors
  • @Mezzle made their first contribution in #15618
  • @kowalski21 made their first contribution in #15701
  • @laaibaQasim made their first contribution in #15258
  • @weknowyourgame made their first contribution in #15881
  • @Floofy6 made their first contribution in #15683

Full Changelog: v2.17.1...v2.17.2

v2.17.1

Highlights
Regression with workers in Redis Event Bus

This release fixes a critical bug introduced in 2.17.0.

What

Not await bullWorker_.run() in event-bus-redis onApplicationStart

Why

bullWorker_.run() is designed to return only when the worker is closed (https://github.com/taskforcesh/bullmq/issues/2128). This bug had flown under the radar until we started awaiting all modules onApplicationStart here. The effect is that when running in worker mode, application startup hangs indefinitely. It does not happen in server mode because bullWorker_ only exists in worker mode.

Bugs
Other Changes
  • i18n(ja): complete dashboard translations — fill 511 missing keys by @greymoth-jp in #15839
New Contributors
  • @greymoth-jp made their first contribution in #15839

Full Changelog: v2.17.0...v2.17.1

v2.17.0

Highlights

IMPORTANT: This release contains a regression around worker instances. You should not upgrade to this but instead 2.17.1.

Global Product Options

🚧 Breaking change

Product options in Medusa can now be global — defined once at the store level and reusable across any number of products. Previously, options such as "Size" or "Color" had to be recreated independently for each product. With this release, you define an option once, attach it to as many products as you need, and manage values from a single place. This unlocks consistent variant modeling across large catalogs and reduces duplication when building storefront filters or admin tooling.

#13817

Read more in the announcement post.


Provider-Agnostic Auth Verification

🚧 Breaking change

Auth verification (email, phone, etc.) has been reworked into a flexible, provider-based system. You can now declare exactly which verifications are required per actor type and auth provider via the new authVerificationsPerActor config — for example, require email verification for customers using emailpass, but skip it for those signing in with Google. Codes are issued and confirmed through pluggable code providers, with a built-in token provider out of the box.

This is a breaking change:

  • The verification endpoints moved from POST /auth/:actor_type/:auth_provider/verification/request (and /confirm) to flat POST /auth/verification/request and POST /auth/verification/confirm. The request route is now authenticated and takes entity_id, entity_type, and code_provider in the body instead of actor/provider in the URL.
  • The JS SDK's auth verification methods were updated to match — upgrade the SDK and adjust any custom verification calls.
  • A database migration replaces the auth_verification_token table with a new auth_verification table. Run npx medusa db:migrate after upgrading; any pending (unconfirmed) verifications are discarded.

#15696


Medusa ESLint Plugin

A new @medusajs/eslint-plugin package ships with this release, bringing first-party linting rules for Medusa projects. The plugin covers API routes, subscribers, scheduled jobs, admin customizations, and module patterns. Lint runs automatically via the Medusa CLI when the plugin is installed:

npx medusa lint

Rules are grouped into config presets (recommended, modules, etc.) so you can opt in to the level of strictness that fits your project.

#15719 #15697 #15700 #15714 #15715 #15717

Features
Bugs
  • fix: handle bodyparser errors by @peterlgh7 in #15749
  • fix: Run linting by default if eslint plugin is installed by @sradevski in #15816
  • fix(medusa): update variant mutation endpoints query config to its retrieve query config by @NicolasGorga in #15735
  • fix(core-flows): include order shipping method names in tax context by @gaoflow in #15783
  • fix(dashboard): don't call hooks after an early return in UserLink by @merkelis-p in #15751
  • fix(dashboard): don't call useTranslation inside NavItem items.map() by @merkelis-p in #15750
  • fix(caching): invalidate list caches on entity update events by @imharjot in #15747
  • fix: log single error log line by @peterlgh7 in #15748
  • fix(medusa): maintain ESLint config detection behavior by @shahednasser in #15776
  • fix: Remove unused actor type in auth module by @sradevski in #15761
  • fix(eslint-plugin): handle link edge cases by @shahednasser in #15774
  • fix(eslint-plugin): fix and improve main config by @shahednasser in #15758
  • fix(eslint-plugin): fixes to avoid false positives by @shahednasser in #15743
  • fix(core-flows, loyalty-plugin, medusa): fix medusa lint errors by @shahednasser in #15732
  • fix(framework,medusa): surface real error and terminate process on db commands by @NicolasGorga in #15726
  • fix(loyalty-plugin): respect user locale in currency formatting by @adem-loghmari in #15725
  • fix(dashboard): feature flag rbac sidebar entries by @NicolasGorga in #15733
  • fix(dashboard): prevent URL param collision breaking pagination in price list add products modal by @Tusharkhadde in #15704
  • fix(pricing): return override as original_amount when a sale is stacked on an override by @cainydev in #15541
  • fix(framework): match build ignore list against path segments by @sapirbaruch in #15577
  • fix(core-flows): scope calculated shipping provider items to the option's shipping profile by @langovoi in #15163
  • fix(promotion): prevent negative taxable base when stacking promotions by @shafi-VM in #15532
  • fix(utils,dashboard): add GMD (Gambian Dalasi) to default currency lists by @kzroo in #15266
  • fix: Await onapplicationstart by @sradevski in #15786
Documentation
Chores
New Contributors

Full Changelog: v2.16.0...v2.17.0

v2.16.0

Highlights

This release comes with many improvements and bug fixes. We highly recommend updating to leverage these changes in your application.

For the Medusa MCP users, you can ask your AI agent to update your project with the following prompt:

update my Medusa project to v2.16.0

It will fetch the necessary changes needed to update your project.

<role>
You are a Medusa upgrade specialist. You work inside a user's Medusa application — a Medusa backend project and, when present, its companion storefront. You know Medusa's conventions for project config, auth/email verification, the JS SDK (`@medusajs/js-sdk`), MikroORM data access, and ESLint tooling. You make no change the user has not approved.
</role>

<task>
Investigate this project and produce a migration plan to upgrade it from its current Medusa version to v2.16.0, then present the plan for the user's approval before making any edits.
</task>

<context>
v2.16.0 is a minor release with several breaking changes that require code or config updates. This prompt covers only the required upgrade steps and breaking changes — additive features in this release (tax line context hook, multi-shipping-method carts, new/custom admin injection zones) are intentionally out of scope; do not implement them.

The breaking changes in scope:

1. **Package version bump to v2.16.0** for all `@medusajs/*` packages.
2. **MikroORM bumped to 6.6.14** (security fix for CVE-2026-44680). `manager.find` now throws on relations that don't exist on an entity instead of silently ignoring them.
3. **`react-router-dom` bumped to `6.30.4`** (defensive security update). Admin customizations may break if not updated.
4. **ESLint plugin** (`@medusajs/eslint-plugin`). New projects ship with it; existing projects should add it. Once configured, `medusa build` and `medusa develop` run linting by default.
5. **Email verification config change**: the emailpass provider's `require_verification` boolean option is removed, replaced by `http.authVerificationsPerActor`.
6. **Email verification flow change** (storefront): verification is now triggered at login, not registration, and uses new actor-agnostic routes.
7. **Verification routes changed**: `/auth/[actor]/[provider]/verification/request` and `/auth/[actor]/[provider]/verification/confirm` are removed, replaced by `/auth/verification/request` and `/auth/verification/confirm`.
8. **JS SDK email-verification signature changes** for `auth.register`, `auth.login`, `auth.verification.request`, and `auth.verification.confirm`.
9. **Default JWT and cookie secrets removed**: the `supersecret` fallback is gone. In production, the app throws and fails to start if `http.jwtSecret` / `http.cookieSecret` are not set.

For anything not covered here, consult the official Medusa documentation at https://docs.medusajs.com or the Medusa MCP server before acting. Do not guess at APIs, config keys, or route shapes — verify them.
</context>

<inputs>
You are given access to the project's working directory. You must discover the following yourself; do not assume:
- **Project shape**: standalone Medusa project vs. monorepo (e.g. `apps/backend` + workspaces). Check for a root `package.json` with workspaces and an `apps/` directory.
- **Storefront presence**: a separate storefront app/repo or directory that uses `@medusajs/js-sdk`. If no storefront is in this workspace, treat storefront steps as guidance to surface to the user, not edits you can make.
- **Current Medusa version**: read from `package.json` dependencies.
- **Whether the project uses email verification**: search for `require_verification`, `authVerificationsPerActor`, `/auth/*/verification/`, `sdk.auth.verification`, or `verification_required`.
- **Whether secrets are configured**: inspect `medusa-config.ts`/`.js` for `http.jwtSecret` / `http.cookieSecret` and the environment for `JWT_SECRET` / `COOKIE_SECRET`.
- **Whether `react-router-dom` is a direct dependency.**
- **Whether custom code calls `manager.find` directly** (raw MikroORM access outside the module service abstractions).
</inputs>

<steps>
Work through these in order. For each, record findings and the proposed change in the plan — do not edit yet.

1. **Detect project shape and current version.** Read the relevant `package.json` files. Note standalone vs. monorepo and the storefront location (if any). Record the current `@medusajs/medusa` version.

2. **Plan the package version bump.** Identify every `@medusajs/*` dependency and devDependency across the backend (and admin/plugin packages if monorepo) and target `2.16.0`. Note that `@medusajs/ui` does not follow the `2.x` line — if it is a direct dependency anywhere (commonly in admin customizations), target `4.1.16` rather than `2.16.0`. If `react-router-dom` is a direct dependency anywhere (commonly in admin customizations or storefront), target `6.30.4`. Plan a single install/upgrade pass and note the package manager in use (yarn/npm/pnpm — detect from lockfile).

3. **Audit JWT and cookie secrets.** Check whether `http.jwtSecret` and `http.cookieSecret` are set in config or via `JWT_SECRET` / `COOKIE_SECRET` env vars. The default `supersecret` fallback is removed; in production a missing value throws at startup and the app fails to boot. If they are unset or still rely on the default, flag this as a **must-fix before deploying** item and propose setting them via environment variables. Never invent or hardcode secret values — instruct the user to generate strong secrets and set the env vars.

4. **Audit direct `manager.find` usage.** Search custom code for direct MikroORM `manager.find` calls that pass `fields`/`populate` referencing relations. Under MikroORM 6.6.14 these now throw if a referenced relation/property does not exist on the entity. For each occurrence, plan to validate field/populate paths against the entity metadata before the call (drop paths that don't map to a real property/relation), mirroring how Medusa prunes them internally. If no direct `manager.find` usage exists, record that this step is N/A.

5. **Plan ESLint plugin setup.** This is strongly recommended; once configured, `medusa build` and `medusa develop` lint by default and `medusa develop` fails to start on lint errors.
   - Add dev dependencies: `@medusajs/eslint-plugin`, `eslint`, and `jiti`. Install at the monorepo root for monorepos, or directly in the project for standalone projects (and in plugins). `jiti` is required: the config is written in TypeScript (`eslint.config.ts`), and ESLint 9 uses `jiti` to load and transpile a TS config file at runtime. Without it, linting fails to load the config.
   - Create the flat config (`eslint.config.ts`, or `.js`/`.mjs`):
     - **Standalone project / plugin:**
       ```ts
       import { defineConfig } from "eslint/config"
       import medusa from "@medusajs/eslint-plugin"

       export default defineConfig([...medusa.configs.recommended])
       ```
     - **Monorepo root** (`eslint.config.ts`): same as above.
     - **Monorepo backend** (\`apps/backend/eslint.config.ts\`): same as above.
   - Add `"eslint.config.*"` to the `exclude` array in the backend `tsconfig.json` to avoid type errors on the config file.
   - Add a `lint` script to the backend's `package.json` so the command is easy to run, e.g. `"lint": "medusa lint"`.
   - Note the new `medusa lint` command (supports `--fix` and `--quiet`) and the `--no-lint` flag for `medusa build` / `medusa develop`. Recommend running `medusa lint --fix` after upgrade and surfacing remaining lint errors to the user.

6. **Plan email verification config migration (backend).** Only if the project uses email verification.
   - Remove the `require_verification` option from the emailpass provider configuration.
   - Add `http.authVerificationsPerActor` under `projectConfig` in `medusa-config.*`. Its type is `Record<actorType, { entity_type: string; auth_provider: string }[]>`. An empty array for an actor type means no verification required. Example:
     ```ts
     http: {
       authVerificationsPerActor: {
         user: [],
         customer: [
           { entity_type: "email", auth_provider: "emailpass" },
         ],
       },
     }
     ```
   - Preserve the project's existing intent: map the previous `require_verification: true/false` (and any per-actor expectations) onto the new per-actor structure. Confirm with the user which actor types require verification if it is ambiguous.

7. **Plan the `auth.verification_requested` subscriber migration (backend).** Only if the project has a subscriber handling the `auth.verification_requested` event (search for `auth.verification_requested` or a `verificationRequestedHandler`). The event payload changed in v2.16.0:
   - `token` is renamed to **`code`** — use `code` to build the verification link.
   - `provider` is renamed to **`code_provider`** (defaults to `"token"`).
   - `actor_type` is **removed**. Replace the `actor_type !== "customer"` guard with an `entity_type` check, e.g. `if (entity_type !== "email") return`.
   - `provider_identity_id` is **removed**.
   - `entity_type` (e.g. `"email"`) and an optional `metadata?: Record<string, unknown>` are **added**. `entity_id` is still the email/identifier.

   Migration example:
   ```ts
   // Before
   export default async function verificationRequestedHandler({
     event: { data: { entity_id: email, token, actor_type } },
     container,
   }: SubscriberArgs<{
     entity_id: string
     token: string
     actor_type: string
     provider: string
     auth_identity_id: string
     provider_identity_id: string
     expires_at: string
   }>) {
     if (actor_type !== "customer") {
       return
     }
     // ...verification_url uses `token`
   }

   // After
   export default async function verificationRequestedHandler({
     event: { data: { entity_id: email, entity_type, code } },
     container,
   }: SubscriberArgs<{
     entity_id: string
     entity_type: string
     code_provider: string
     auth_identity_id: string
     code: string
     expires_at: string
     metadata?: Record<string, unknown>
   }>) {
     // only handle email verifications.
     if (entity_type !== "email") {
       return
     }
     // ...verification_url uses `code` instead of `token`
   }
   ```
   Update every reference inside the handler that used `token` to use `code` (including the `verification_url` built for the notification).

8. **Plan verification route migration.** Find any backend custom code, middleware, or storefront calls referencing the removed routes:
   - `/auth/[actor]/[provider]/verification/request` → `/auth/verification/request`
     - New request body: `{ entity_id: string, entity_type: string, code_provider?: string, metadata?: Record<string, unknown> }`. Response: `{ verification }`.
   - `/auth/[actor]/[provider]/verification/confirm` → `/auth/verification/confirm`
     - New request body: `{ code: string, code_provider?: string }`. Response: `{ entity_id, entity_type, code_provider, verified_at }`.

9. **Plan JS SDK signature migration (storefront / SDK consumers).** Update calls to match v2.16.0:
   - `auth.register(actor, method, payload)` — the previous `options` / `returnVerification` parameter is removed. The register call no longer reports whether verification is required.
   - `auth.login(actor, method, payload)` — now may return `{ verification_required: true, verification?, token }`. Verification is detected here, not at registration.
   - `auth.verification.request(body)` — signature changed to a single body object: `{ entity_id, entity_type, code_provider?, metadata? }` (no more `actor`/`method` positional args).
   - `auth.verification.confirm(body)` — signature changed to `{ code, code_provider? }` (token is passed as `code`).

   Migration examples:
   ```ts
   // Before
   const { verification_required } = await sdk.auth.register("customer", "emailpass", payload, { returnVerification: true })
   // After
   await sdk.auth.register("customer", "emailpass", payload)
   const loginResult = await sdk.auth.login("customer", "emailpass", payload)
   // loginResult may be { verification_required, token } when verification is needed

   // Before
   await sdk.auth.verification.request("customer", "emailpass", { entity_id: "customer@gmail.com" })
   await sdk.auth.verification.confirm("customer", "emailpass", { token })
   // After
   await sdk.auth.verification.request({ entity_id: "customer@gmail.com", entity_type: "email" })
   await sdk.auth.verification.confirm({ code: token })
   ```

10. **Plan storefront verification flow migration.** Only if a storefront with email verification exists. The flow moved verification from after registration to after login:
   - **Old flow:** register → register response says verification required → request verification → confirm → create customer + login.
   - **New flow:** register (get registration token) → redirect to login → login → login response says `verification_required` → call `/auth/verification/request` → user opens verify page with token → `/auth/verification/confirm` with `{ code: token }` → login again → create customer if needed → login again.
   - Plan the storefront page/route changes to implement the new ordering and the new SDK signatures from steps 8–9. If the storefront is a Medusa starter, note that updated starters already include this flow and the user may prefer to diff against the latest starter.

11. **Compile and present the migration plan** (see `<output_format>`). Then stop and wait for the user's approval. Do not edit any files until the user approves. If the user approves, apply the changes in dependency order (version bump and install first, then config, then code), and after each significant change note how to verify it.
</steps>

<constraints>
- Produce a plan first; make no file edits until the user explicitly approves it.
- Cover only the required/breaking changes listed above. Do not implement additive features (tax line context hook, multi-shipping carts, injection zones) even if you notice opportunities — mention them only if the user asks.
- Never hardcode or generate secret values into config or committed files. JWT/cookie secrets must come from environment variables; instruct the user to set strong values.
- Verify every API, route, config key, and SDK signature against https://docs.medusajs.com or the Medusa MCP server before relying on it. Do not guess.
- Match the project's existing code style (this is a TypeScript Medusa project: no semicolons, double quotes, 2-space indentation, parens on arrow functions). Never use emojis.
- If the storefront is not in this workspace, do not fabricate edits to it — surface the storefront steps as instructions for the user to apply in their storefront repo.
- Detect the package manager from the lockfile and use it consistently; do not assume npm.
</constraints>

<error_handling>
- If you cannot determine whether the project uses email verification, the package manager, or which actor types need verification, ask the user a targeted question rather than guessing.
- If a referenced file (e.g. `medusa-config.ts`, `tsconfig.json`) is missing or has an unexpected shape, report what you found and ask how to proceed instead of forcing an edit.
- If `manager.find` usage is ambiguous (e.g. dynamic field lists), flag it for manual review in the plan rather than rewriting it blindly.
- If the current version is already >= 2.16.0, report that and stop; do not downgrade or re-apply migrations.
</error_handling>

<output_format>
Present the plan as Markdown with these sections:

1. **Project summary** — shape (standalone/monorepo), current Medusa version, storefront presence, package manager.
2. **Applicable changes** — a checklist table: each in-scope change, whether it applies to this project (Yes/No/N/A), and a one-line reason.
3. **Proposed changes, in order** — for each applicable change: the files affected, the concrete edit (with before/after snippets where useful), and the command(s) to run.
4. **Must-fix before deploy** — items that will break a production boot or build if skipped (e.g. missing JWT/cookie secrets, lint errors blocking `medusa develop`).
5. **Manual verification steps** — how the user confirms each change worked after applying (e.g. `medusa build`, `medusa lint`, register/login/verify a test customer end-to-end).
6. **Out of scope / notes** — additive features intentionally skipped, and any storefront steps the user must apply in a separate repo.

End with an explicit line asking the user to approve the plan before you apply any changes.
</output_format>

<success_criteria>
- The plan covers every in-scope breaking change that applies to the project, and explicitly marks the rest N/A with a reason.
- No file is edited before user approval.
- Every proposed config key, route path, and SDK signature matches v2.16.0 (verified against the docs/MCP, not assumed).
- Secrets are handled via environment variables, never hardcoded.
- After approval and application, `medusa build` succeeds, `medusa lint` reports no errors, and (if email verification is used) a test register → login → verify → login flow completes end-to-end.
</success_criteria>
ESLint Plugin

To make development easier with Medusa, both for developers and AI agents, we've added a new ESLint plugin for Medusa applications. It catches issues that are either against Medusa conventions or may cause runtime errors if undetected.

All new Medusa projects come with the ESLint plugin installed by default. For existing users, we recommend installing it in your project as well. You can install the plugin without updating to this version of Medusa.

Learn more about the plugin in this documentation.

Install ESLint Plugin

To use the plugin, install it as a dev dependency along with ESLint-related plugins. For monorepo projects, install it in the root of the monorepo. For standalone Medusa projects, install it directly in the project:

// or use your package manager
pnpm install --save-dev @medusajs/eslint-plugin eslint jiti
Configure ESLint Plugin in Monorepo Projects

In your monorepository root, create eslint.config.ts with the following content:

import { defineConfig } from "eslint/config"
import medusa from "@medusajs/eslint-plugin"

export default defineConfig([...medusa.configs.recommended])

Then, create the file apps/backend/eslint.config.ts with the following content:

import { defineConfig } from "eslint/config"
import medusa from "@medusajs/eslint-plugin"

export default defineConfig([...medusa.configs.recommended])

Also, add eslint.config.* to the excluded files in apps/backend/tsconfig.json to avoid type errors on the file:

{
  "exclude": [
    // ...
    "eslint.config.*",
  ]
}
Configure ESLint Plugin in Standalone Project

In your standalone project (and plugins), create eslint.config.ts with the following content:

import { defineConfig } from "eslint/config"
import medusa from "@medusajs/eslint-plugin"

export default defineConfig([...medusa.configs.recommended])
Lint Project

The linting now works with eslint and your IDE if you have the ESLint plugin installed. If you upgrade to this version, you can also use Medusa's CLI for linting as explained in the next section.

Linting in Medusa Commands

🚧 Breaking change

If you setup the ESLint plugin and config in your Medusa backend, linting will run by default in the medusa build and medusa develop commands. If there are lint errors, both commands will fail.

You can disable the linting with the --no-lint flag on both commands:

pnpm medusa build --no-lint
pnpm medusa develop --no-lint

This release also includes a new lint command that runs the linting based on your configurations:

pnpm medusa lint

You can also pass options like --fix and --quiet.

Learn more in the CLI documentation.

MikroORM version bump to 6.6.14

🚧 Breaking change

MikroORM 6.6.12 is affected by CVE-2026-44680. This doesn't impact Medusa applications by default, but can affect Medusa customizations if they're JSON inputs are not properly validated with Zod.

This version updates MikroORM to 6.6.14. This comes with a breaking change in how MikroORM's manager.find method works. Previously, it ignored all relations that don't exist on an entity. Now, it will fail with an error.

This has been resolved internally by this PR. However, if you use manager.find in your code, we encourage you to look at the changes made in the PR and apply them in your code.

react-router-dom update

🚧 Breaking change

react-router-dom has also been updated to 6.30.4 due to a recent advisory. The previous version didn't impact Medusa applications; the update is a defensive measure only.

If you have react-router-dom installed in your application, make sure to update it to 6.30.4. Otherwise, admin customizations may break.

Email Verification Changes

🚧 Breaking change

Email verification has been expanded to support for general verification for any user information, such as email or phone number. Also, it now supports specifying different verification requirement per actor type.

In the previous version, The require_verification option of the Emailpass provider accepted a boolean value to enable or disable verification for all actor types. Now, this option has been removed. Instead, use the http.authVerificationsPerActor:

// medusa-config.ts

module.exports = defineConfig({
  projectConfig: {
    http: {
      authVerificationsPerActor: {
        user: [], // No verification required for users
        customer: [
          {
            entity_type: "email",
            auth_provider: "emailpass",
          }
        ]
      },
    },
    // ...
  },
  // ...
})

In this example, you require verification only for customers. The verification is for their email.

Learn more about setting up email verification in the backend in this documentation

Email Verification Flow Change

🚧 Breaking change

Following the above change, the email verification flow has now changed.

The default Medusa starter (dtc-starter) has been updated to include this new flow in the storefront by default.

In the previous version, you implemented the following flow for email verification in the storefront:

  1. Send a request to /auth/customer/emailpass/register to obtain a registration token.
  2. Request returns that the verification is required.
  3. Send a request to the /auth/customer/emailpass/verification/request route to request a verification token for the customer.
  4. The customer goes to the verification page in the storefront, with the token in the query parameter
  5. Send a request to the /auth/customer/emailpass/verification/confirm route to confirm the verification
  6. If verification is successful, create customer + login

In this version, the flow has changed to the following:

  1. Send a request to /auth/customer/emailpass/register to obtain a registration token.
  2. Request returns a successful response. Redirect the customer to the login page.
  3. Customer logs in. Send a request to /auth/customer/emailpass.
  4. Request returns that the verification is required.
  5. Send a request to the /auth/verification/request to request a verification token for the customer.
  6. The customer goes to the verification page in the storefront, with the token in the query parameter
  7. Send a request to the /auth/verification/confirm route to confirm the verification
  8. If verification is successful, create customer + login

Learn more about this new flow and how to implement it in this documentation

Change in Verification Routes

🚧 Breaking change

Following the above change in email verification flow, the following routes have been removed:

  • /auth/[actor]/[provider]/verification/request
  • /auth/[actor]/[provider]/verification/confirm

They've been replaced by the following routes:

  • /auth/verification/request
  • /auth/verification/confirm

Refer to the Admin and Store API references to learn more about these routes.

Change in JS SDK signature for email verification

🚧 Breaking change

The previous version introduced a new options parameter for the JS SDK's auth.register method that allows to configure the behavior when a registering user requires verification first. This option has been removed since the register route no longer returns whether verification is required, as explained in the verification flow changes section.

Instead, the sdk.auth.login route returns whether verification is required now. For example:

// Before
const { verification_required } = await sdk.auth.register(
  "customer",
  "emailpass",
  {
    email: "customer@gmail.com",
    password: "supersecret"
  },
  {
    returnVerification: true
  }
)

// After
// get registration token
await sdk.auth.register(
  "customer",
  "emailpass",
  {
    email: "customer@gmail.com",
    password: "supersecret"
  },
)

// later, login
const { verification_required } = await sdk.auth.login(
  "customer",
  "emailpass",
  {
    email: "customer@gmail.com",
    password: "supersecret"
  },
)

In addition, the signatures of the auth.verification.request and auth.verification.confirm have changed due to the changes to their routes:

// before
await sdk.auth.verification.request("customer", "emailpass", {
  entity_id: "customer@gmail.com"
})
await sdk.auth.verification.confirm("customer", "emailpass", {
  token
})

// after
await sdk.auth.verification.request({
  entity_id: "customer@gmail.com",
  entity_type: "email",
})
await sdk.auth.verification.confirm({ code: token })
Tax Line Context Hook

A new setTaxLineContext hook allows customization of tax calculation context, providing greater flexibility for complex tax scenarios. The hook is added to the cart's upsertTaxLinesWorkflow and updateTaxLinesWorkflow workflows, and the order's updateOrderTaxLinesWorkflow.

For example:

import { updateOrderTaxLinesWorkflow } from "@medusajs/medusa/core-flows";

updateOrderTaxLinesWorkflow.hooks.setTaxLineContext(
  async ({ order, items, shipping_methods }, { container }) => {
    // do something
  }
)
Multi-Shipping Method Cart Support

Carts now support multiple shipping methods of different shipping profiles, enabling split shipments and complex fulfillment scenarios. This allows customers to choose different shipping options for different items within the same order.

You can now pass an array of shipping methods to add to the cart in the /store/carts/:id/shipping-methods API route:

sdk.store.cart.addShippingMethod("cart_123", [
   // option 1 with shipping profile 1
   {
    option_id: "so_123",
    data: {
      // custom data for fulfillment provider.
    }
  },
   // option 2 with shipping profile 2
   {
    option_id: "so_456",
    data: {
      // custom data for fulfillment provider.
    }
  }
])
New Injection Zones

This release adds the following new injection zones in the admin dashboard:

// draft order pages
"draft_order.details.before"
"draft_order.details.after"
"draft_order.details.side.before"
"draft_order.details.side.after"
"draft_order.list.before"
"draft_order.list.after"

// gift card pages
"gift_card.details.before"
"gift_card.details.after"
"gift_card.details.side.before"
"gift_card.details.side.after"
"gift_card.list.before"
"gift_card.list.after"
"gift_card.list.side.before"
"gift_card.list.side.after"

// store credit account pages
"store_credit_account.details.before"
"store_credit_account.details.after"
"store_credit_account.details.side.before"
"store_credit_account.details.side.after"
"store_credit_account.list.before"
"store_credit_account.list.after"
Custom Injection Zones

The admin dashboard now supports adding custom injection zones in custom pages. This allows plugin developers to add widget injection zones in their UI routes.

Learn more in the Custom Injection Zones documentation.

Other dependency patches

This release bumps dependencies for OpenTelemetry and UUID due to recent security advisories in these packages. Medusa applications before this version are not impacted by these advisories, as we don't use any of the affected code, so this is a defensive measure only.

Tighten Payment webhook handling

This release handles edge cases when a payment provider's getWebhookActionAndData returns an undefined data.session_id field. Previously, this led to an arbitrary payment session being processed. Now, webhook handling exists early.

Properly escape JSON containment filters with Index Module

This release properly escapes single quotes in filters passed to the /store/products route when the Index Module is used. Previously, passing single quotes in filters like title caused SQL syntax errors. Now, single quotes are escaped in those filters.

Removed default JWT and Cookie secrets

This release removes the default supersecret value set for JWT and cookie secrets. In production, if those keys aren't set manually, the application will throw an error and fail to start.

Features
Bugs
  • fix(cli): replace unmaintained pg-god with direct pg.Pool CREATE DATA… by @dsumeet14 in #15635
  • fix: product organization combobox search and cache invalidation by @PrathamRanka in #15634
  • fix(dashboard): enforce locale-specific plural forms in translations by @radeknapora in #13268
  • fix(index): subscribe to restored events and re-index on restore by @Tusharkhadde in #15654
  • fix(payment-stripe): warn when webhookSecret is missing by @dsumeet14 in #15684
  • fix(dashboard): fix refund form rounding for sub-cent amounts by @Metbcy in #15349
  • fix(dashboard): prevent duplicate items in return create form by @Metbcy in #15348
  • fix: pin peter-evans/create-pull-request to a commit SHA (supply-chain hardening) by @eliottreich in #15664
  • fix(js-sdk): add RBAC ESM import extensions by @puneetdixit200 in #15597
  • fix(core-flows): skip tax on gift card line items when cart taxes are recalculated by @cainydev in #15545
  • fix(utils): InjectManager error message shows "undefined" instead of "baseRepository_" by @Tusharkhadde in #15662
  • fix(core-flows): apply customer-group price lists when creating a dra… by @shafi-VM in #15538
  • fix(medusa): allow updating buyget promotions via PATCH /admin/promotions/:id by @sanidhyasin in #15619
  • fix(medusa): type product category create response by @Zbl1007 in #15588
  • fix: correct "occured" → "occurred" typo in error messages and docs by @DucMinhNe in #15549
  • fix(product): Support non-Latin characters in product handles with Un… by @Cpidar in #15346
  • fix(admin): fix Copy API Key behavior in secret key created dialog by @Tusharkhadde in #15531
  • fix(plugin:develop): invoke yalc via process.execPath on Windows by @addemod in #15523
  • fix(medusa): clear session cookie on DELETE /auth/session by @shafi-VM in #15510
  • fix(medusa): include address_name in customer address defaults by @puneetdixit200 in #15511
  • fix(utils): preserve context index overrides by @puneetdixit200 in #15503
  • fix(core-flows): include shipping_method.name in tax calculation context by @NicolasGorga in #15575
  • fix(index): use parameterized binding for JSONB containment filter by @shahednasser in #15599
  • fix(framework): change for session cookies by @shahednasser in #15601
  • fix(utils,product): handle array of mikroORM entities serialization and fix variant image assignment serialization issue by @NicolasGorga in #15591
  • fix: use unique order country column id by @xonaib in #15486
  • fix: support csv mime types by @aritradhabal in #15483
  • fix(dashboard): preserve zero-amount shipping option prices on re-save by @Ultron03 in #15468
  • fix(dashboard): prevent crash when deleting the last image from product media gallery by @appinteractive in #15444
  • fix(file-local, core-flows): improve file resolution + invalid csv file handling by @shahednasser in #15558
Documentation
Chores
Other Changes
New Contributors
  • @dsumeet14 made their first contribution in #15635
  • @PrathamRanka made their first contribution in #15634
  • @archievi made their first contribution in #15637
  • @radeknapora made their first contribution in #13268
  • @Tusharkhadde made their first contribution in #15654
  • @NicolasGorga made their first contribution in #15685
  • @Metbcy made their first contribution in #15349
  • @eliottreich made their first contribution in #15664
  • @puneetdixit200 made their first contribution in #15597
  • @cainydev made their first contribution in #15545
  • @shafi-VM made their first contribution in #15538
  • @sanidhyasin made their first contribution in #15619
  • @leobenzol made their first contribution in #15595
  • @Zbl1007 made their first contribution in #15588
  • @GBreg19 made their first contribution in #15561
  • @DucMinhNe made their first contribution in #15549
  • @Jeri436 made their first contribution in #15543
  • @Cpidar made their first contribution in #15346
  • @addemod made their first contribution in #15523
  • @christiananese made their first contribution in #15584
  • @xonaib made their first contribution in #15486
  • @aritradhabal made their first contribution in #15483
  • @bodasooqa made their first contribution in #15567
  • @Ultron03 made their first contribution in #15468
  • @appinteractive made their first contribution in #15444

Full Changelog: v2.15.5...v2.16.0

v2.15.5

Highlights
Multi-Factor Authentication

Medusa now supports multi-factor authentication (MFA). The admin dashboard includes a complete MFA UI that allows users to set up and manage their authentication methods. MFA lifecycle events are now emitted for tracking authentication flows.

After updating, make sure to set the AUTH_MFA_ENCRYPTION_KEY environment variable to a random 64-character string:

AUTH_MFA_ENCRYPTION_KEY=your_random_64_character_string

Also, if you've added the Auth Module to your medusa-config.ts file to set any of its options, make sure to set the mfa.encryption_key option to the same environment variable:

import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        mfa: {
          encryption_key: process.env.AUTH_MFA_ENCRYPTION_KEY,
        },
        // other options...
      },
    },
  ],
})

If you don't set the mfa.encryption_key option, you'll get a "MFA encryption key is required to use MFA methods" error whenever trying to enroll or verify an MFA factor.

#15496 #15493 #15495

Features
Bugs
  • fix(core-flows): avoid refunding captures made in separate completeCartWorkflow executions by @NicolasGorga in #15527
  • fix(utils): add mfa to inline snapshot test assertion by @NicolasGorga in #15518
  • fix(core-flows): respect allow_backorder when calculating pickup inventory availability by @marlinjai in #15440
  • Allow cancelling pending MFA setup by @christiananese in #15475
  • fix(dashboard): order list status badges show correct colors when view_configurations is enabled by @shiminshen in #15430
  • fix(core-flows): use hasPermission util to perform checks in validateUserRolePermissionsStep by @NicolasGorga in #15470
  • fix(core-flows,medusa): align validate user permissions check with hasPermission util by @NicolasGorga in #15465
Documentation
Chores

Full Changelog: v2.15.3...v2.15.5

v2.15.3

Highlights
Multi-Factor Authentication Support

This release adds the primitives to support Multi-Factor Authentication (MFA) for enhanced security. This includes new authentication provider primitives, API routes for MFA management, and retrieval functionality. The implementation provides a foundation for integrating various MFA methods.


Promotion Code Visibility Improvements

When promotion codes are skipped due to budget or usage limits, the system now surfaces this information to provide better visibility into why certain promotions weren't applied. This helps merchants understand promotion application behavior and troubleshoot issues.

#15396

Features
Bugs
  • fix(design-system): broaden React peer dependencies to support v18 an… by @Suh0161 in #15271
  • fix(core-flows): harden create payment sessions when customer has no account holders by @Suh0161 in #15264
  • fix(dashboard): include inventory query in detail key by @Derekko-web in #15417
  • fix(dashboard): complete and correct Thai (th) translations by @Ligament in #15409
  • fix(test-utils, link-modules): encode URL credentials and fix schema-qualified RENAME TO by @Ultron03 in #15344
  • fix(medusa): fix filtering by categories and tags in /store/products with the index module by @shahednasser in #15405
  • fix(create-medusa-app): fix incorrect command replacement when using yarn and npm by @shahednasser in #15436
  • fix(utils): implement tokenized free text search by @Suh0161 in #15275
  • fix(core-flows): fix incorrect stock location picked for item with backorder in a sales channel with multiple locations by @shahednasser in #15159
Documentation
Chores

Full Changelog: v2.15.2...v2.15.3

v2.15.2

Highlights
Fix Migrations Regressions

Following the MikroORM update in v2.13.6, a regression was introduced where running medusa db:migrate generated a full database snapshot for custom modules. Then, when running medusa db:generate later, a bug in the migration generation caused the module snapshot to be overridden by the database snapshot, which would then generate migrations dropping all the tables in the database.

We encourage all users on a version after v2.13.6 to update to this release to mitigate database issues.

After updating, please inspect the migrations/.snapshot.**.json files in your custom modules and:

  • Delete any snapshots with the name .snapshot-<db-name>.json.
  • For module snapshots (.snapshot-<module-name>.json), confirm that they don't include table names outside of your module, such as product, order, etc... if so, delete those snapshots and regenerate again with medusa db:generate.

You can also use the following prompt with your AI agent:

# Cleaning Up Incorrect Snapshot Files

A bug in earlier versions of Medusa caused `db:migrate` to generate snapshot files named after the database (e.g. `.snapshot-medusa.json`) instead of the module. In some cases, running `db:generate` afterward would overwrite the correct module snapshot with this full-database snapshot, causing the next generated migration to contain `DROP TABLE` statements for all Medusa core tables.

Follow the steps below after updating to v2.15.2 that includes this fix.

## Step 1: Delete database-name snapshots

Look in each module's `migrations/` directory for snapshot files named after your database rather than the module. These are incorrectly generated files and should be deleted.

They look like:

```
.snapshot-medusa.json
.snapshot-my-store.json
.snapshot-medusa-medusa-resolutions.json
```

The correct snapshot is named after the module:

```
.snapshot-<module-name>.json
```

Delete any file that does not follow the module-name pattern.

## Step 2: Check for corrupted module snapshots

Open the remaining `.snapshot-<module>.json` file for each module and check its `tables` array. It should only contain tables owned by that module.

If it contains tables from other modules (e.g. `order`, `cart`, `product`, `customer` inside a small custom module), the snapshot was overwritten by the full-database snapshot and is corrupted.

## Step 3: Regenerate corrupted snapshots

For each corrupted snapshot:

1. Delete the corrupted snapshot file.
2. Regenerate it by running:

```bash
npx medusa db:generate <moduleName>
```

## Step 4: Review the regenerated migration

Open the newly created migration file and check its contents. If it contains `DROP TABLE` or unexpected `ALTER TABLE` statements for tables that belong to other modules:

1. **Do not run this migration.**
2. Delete the migration file.
3. The snapshot has now been regenerated correctly — subsequent runs of `db:generate` will produce accurate migrations.
Features
  • feat(medusa): enable index when querying products via promotion attribute values and enable sku search in products entrypoint by @NicolasGorga in #15386
Bugs
Chores

Full Changelog: v2.15.1...v2.15.2

v2.15.1

Highlights
Includes missing PRs from 2.15.0 release

Due to an issue during the release process of 2.15.0, several PRs listed in the release notes weren't included. If you've already installed 2.15.0, you might have experienced behavior matching this description. If you want the changes listed in the release notes of said version, you should install this one instead. The issue in the release process has already been corrected.

Chores

Full Changelog: v2.15.0...v2.15.1

v2.15.0

Highlights
Missing PRs in published packages

Due to an issue during the release process, we identified several PRs listed in these release notes missing from the published packages. We recommend installing 2.15.1 instead.

Aligned Product and Variant Attribute Types

🚧 Breaking change

This version aligns the width, length, height, and weight properties on the Product and ProductVariant data models to be of type float. In previous versions, their type was text on the Product data model, and number on the ProductVariant data model.

This change only affects customizations that relied on the previous text type for these attributes, which are now expected to be float values.


Medusa Cloud Proxy Command

The Medusa CLI now includes a mcloud proxy command for working with Medusa Cloud deployments, streamlining local development workflows against cloud environments.

medusa mcloud proxy

#15320


Promotion Context Hooks

The promotion computation system now supports custom context hooks, allowing developers to pass additional context data when calculating promotions. This enables more sophisticated promotion rules based on external data sources or custom business logic.

Example:

// in src/workflows/hooks/promotion.ts
import { StepResponse } from "@medusajs/framework/workflows-sdk";
import { updateCartPromotionsWorkflow } from "@medusajs/medusa/core-flows";

updateCartPromotionsWorkflow.hooks.setPromotionContext(
  ({ cart }, { container }) => {
    if (cart.items.length >= 2) {
      return new StepResponse({
        company_id: "company_123"
      });
    }
  }
);

#15301


Product SKU Search Support

The admin API now supports searching products by SKU, making it easier to find and manage products through their stock keeping unit identifiers.

#13930


Translation Copy Actions

The admin dashboard translation edit page now includes copy actions for original translations, improving the translation workflow by allowing translators to quickly copy base text.

#14943

Features
  • feat(core-flows): add setPromotionContext hook to pass additional context for promotion computation by @NicolasGorga in #15301
  • feat: Add mcloud proxy command to medusa CLI by @sradevski in #15320
  • feat(product): add SKU search support to admin API by @bqst in #13930
  • feat(dashboard): add copy action for Original translations on translations edit page by @LukasKri in #14943
  • fix(core-flows, payment, types): expose metadata on refund creation through refundPaymentsWorkflow by @Metbcy in #15273
Bugs
  • fix(index): backfill missing inSchemaRef for existing parent entries by @ranma-dev in #15131
  • fix(workflows-sdk): flatten WorkflowData type to remove recursive expansion that caused TS excessive stack depth errors in large consumer codebases by @NicolasGorga in #15174
  • fix(settings): mark Order computed status fields as non-filterable by @aliaksei-loi in #15262
  • fix(utils): preserve casing in toCamelCase for PascalCase identifiers with digits by @Chicolll in #15293
  • fix(dashboard): auto-select currency row when its tax-inclusive toggle is enabled by @Anexus5919 in #15164
  • fix(core-flows): populate delivery_address on return fulfillments by @ShriyansSharma27 in #15241
  • fix(core-flows): validate cart has items before completing by @rnagulapalle in #15231
  • fix(utils): skip empty string translations when applying locale by @asynchroza in #15013
  • fix(medusa): prevent plugin:db:generate crash when model files export by @shubhamchoudhary-2003 in #14718
  • fix(order): fix regression on order.items.metadata showing null when line item metadata is set by @NicolasGorga in #15329
  • fix(core-flows): apply currency-precision tolerance to refundPayments by @hunnyboy1217 in #15316
  • Fixes #14793 -- paymentService_ was not getting updated with the data causing the issue by @Ultron03 in #15295
  • fix(index,modules-sdk,): introduce locking to avoid race conditions in distributed systems for migrations, links and index partition creation by @NicolasGorga in #15176
  • fix(i18n): refresh Czech (cs) translations by @dugynoo in #15286
  • fix(pricing): calculatePrices() with empty context selects prices from pricelists that explicitly have rules by @ShriyansSharma27 in #15277
  • fix(product, dashboard): align product and variant volumetric attributes data types by @NicolasGorga in #14762
  • fix(core-flows,payment): Fix rounding issue on refund creation by @NicolasGorga in #15303
  • fix(run-scripts): load WorkflowLoader during db:migrate so workflow hooks are registered by @AKIB473 in #15260
  • fix(admin): correct settings-related routes in global search results by @biocodersin in #14853
  • fix(file-s3): encode URL path segments individually to preserve prefix slashes by @aayushbaluni in #15109
Documentation
Chores
New Contributors
  • @ranma-dev made their first contribution in #15131
  • @Anexus5919 made their first contribution in #15164
  • @ShriyansSharma27 made their first contribution in #15241
  • @asynchroza made their first contribution in #15013
  • @LukasKri made their first contribution in #14943
  • @shubhamchoudhary-2003 made their first contribution in #14718
  • @hunnyboy1217 made their first contribution in #15316
  • @Ultron03 made their first contribution in #15295
  • @dugynoo made their first contribution in #15286
  • @sradevski made their first contribution in #15320
  • @AKIB473 made their first contribution in #15260
  • @bqst made their first contribution in #13930
  • @biocodersin made their first contribution in #14853
  • @aayushbaluni made their first contribution in #15109
  • @Metbcy made their first contribution in #15273

Full Changelog: v2.14.2...v2.15.0