v2.20.0

medusajs/medusav2.20.0Sep 2, 2026by medusa-os-bot[bot]

AI Summary

This release focuses on security enhancements, including payment provider validation and MFA enforcement, alongside performance improvements via relation expansion limits. It introduces new features like fractional inventory and calculated shipping options in draft orders, while removing the in-memory local search provider.

Key Highlights

  • Security: Payment providers are now validated against the cart's region to prevent unauthorized provider usage.
  • Security: MFA challenge enforcement is now required on MFA routes.
  • Performance: Store API routes now enforce a default relations limit to prevent deep nesting performance issues.
  • New: Support for fractional inventory quantities and units of measure.
  • New: Calculated shipping options are now available in draft orders.

Breaking Changes

  • Relations limit on store routes now defaults to 3 levels (4 for product routes).
  • In-memory local search provider (`@medusajs/search-local`) has been removed.
  • `searchMany` method moved to the search provider interface.
  • MFA routes now require a completed challenge.

New Features

  • Fractional Inventory Quantities and Units of Measure.
  • Calculated Shipping Options in Draft Orders.
  • Vector-based search for Medusa Search.
  • New ESLint plugin workflow rules.

Full Release Notes

<!--

![v2.20.0](https://res.cloudinary.com/dza7lstvk/image/upload/v1788362836/Releases/v2-20-0-471305.png)

# v2.20.0: Security Fixes, Performance Improvements, Calculated Shipping Options in Draft Orders 

v2.20.0 ships security fixes for payment providers per regions and multi-factor authentication. It also includes performance improvements for store API routes by adding limits on relation expansions, support for calculated shipping options in draft orders, and more.

CMS_BREAK -->

## Highlights

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

```bash
Update my Medusa project to v2.20.0
```

### Payment providers are now validated against the cart's region

Security fix: creating a payment session no longer trusts the payment provider ID passed in the request. The `createPaymentSessionsWorkflow` and the cart payment validation step now verify that the chosen provider is actually enabled in the cart's (or payment collection's) region, and reject the request otherwise. Previously a store customer could initiate a payment session with any payment provider installed in the application, even one that was not linked to their region.

No action is required, but if your storefront relied on passing an arbitrary provider ID, make sure the provider is linked to the region the cart belongs to.

---

### MFA Challenge Enforcement on MFA Routes

Security fix: An attacker who already had a user's valid password could use that token to regenerate the recovery codes through `POST /auth/mfa/recovery-codes` and use one of them to complete the challenge, or enroll and delete MFA factors. This releases fixes this issue by tracking whether a user has completed MFA before allowing them to access the recovery codes route.

---

### Relations Limit on Store API Routes

🚧 Breaking change

> This only affects Store API requests that expand deeply nested relations through `fields`. Admin routes are unaffected.

Store API routes now reject requests that expand more than three levels of relations in a single query by default, since deeply nested expansions can cause bad performance. A request such as `fields=*products.variants.options.values` now returns a `400` error listing the offending fields.

The product routes are the only core Store routes that raise the limit above the default, allowing four levels of relations:

- `GET /store/products`
- `GET /store/products/:id`

To change the limit for every Store route, set `storeRelationsLimit` in your HTTP configuration:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeRelationsLimit: 4,
    },
  },
})
```

To change it for a specific route, set `storeRelationsLimit` in the query configuration passed to `validateAndTransformQuery` in your middlewares. It takes precedence over the application-wide configuration:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares, validateAndTransformQuery } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/custom",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(GetCustomSchema, {
          defaults: ["id", "*items"],
          storeRelationsLimit: 5,
          isList: true,
        }),
      ],
    },
  ],
})
```

---

### Redis Caching Storage Format

> This only affects projects using the `@medusajs/caching-redis` provider.

The Redis caching provider now stores entries under a 64-bit hash key and uses a simplified storage mechanism. Existing cache entries written by earlier versions are not readable in the new format and are effectively invalidated on upgrade, so expect a cold cache after deploying. No configuration change is required.

---

### Fractional Inventory Quantities and Units of Measure

Inventory items can now be tracked in fractional quantities with an associated unit of measure, unlocking use cases such as selling by weight, length, or volume. The support spans the Inventory Module, link modules, API routes, and workflows, and the admin dashboard surfaces the unit of measure across the inventory screens.

---

### Calculated Shipping Options in Draft Orders

Draft orders now support shipping options with calculated prices, so third-party rate providers can be used when an operator builds an order manually. A new `setCalculatedShippingPricingContext` hook lets you inject additional data into the pricing context sent to the fulfillment provider when calculating those rates.

---

## Breaking Changes

- **Relations limit on store routes** — Store API routes now enforce a default relations limit to cap query depth and improve performance. Requests that relied on unbounded relation expansion may see fewer nested associations returned. Run `db:migrate` after upgrading. ([#16688](https://github.com/medusajs/medusa/pull/16688))

- **In-memory local search provider removed** — `@medusajs/search-local` (the Orama-backed in-memory provider shipped in v2.19) has been removed. Projects must switch to `@medusajs/search-postgres`, the Medusa Cloud provider, or a custom provider. Search indexes are no longer created at application startup; run `db:migrate` to create them. ([#16545](https://github.com/medusajs/medusa/pull/16545))

- **`searchMany` moved to the provider interface** — The `searchMany` method has been pushed down to the search provider interface so each provider can optimize batch queries internally. Custom search providers must implement `searchMany`. ([#16643](https://github.com/medusajs/medusa/pull/16643))

- **MFA routes now require a completed challenge** — MFA-protected routes now validate that the MFA challenge has actually been completed before granting access, hardening the authentication flow. ([#16610](https://github.com/medusajs/medusa/pull/16610))

---

## Other Changes

### Features

- **Vector-based search for Medusa Search** — Added foundational vector search support including namespace management, vector ingestion, and improved search-definition APIs. ([#16631](https://github.com/medusajs/medusa/pull/16631), [#16673](https://github.com/medusajs/medusa/pull/16673), [#16676](https://github.com/medusajs/medusa/pull/16676))

- **ESLint plugin: new workflow rules** — Three new rules for `@medusajs/eslint-plugin`: `no-nested-when-then` catches nested `when/then` blocks that silently do not compose correctly; `missing-when-name` requires all `when` calls to have a `name` argument for better traceability; `throw-in-workflow-definition` disallows `throw` statements in workflow definition functions, which must use `MedusaError` in steps instead. ([#16520](https://github.com/medusajs/medusa/pull/16520), [#16585](https://github.com/medusajs/medusa/pull/16585), [#16517](https://github.com/medusajs/medusa/pull/16517))

### Bug Fixes

- **Payment provider validation on cart** — Validate that the payment provider selected for a cart actually belongs to the cart's region before processing. ([#16690](https://github.com/medusajs/medusa/pull/16690))

- **Store order routes field expansion** — Blocked field-expansion pivot paths on store order routes to prevent information leakage. ([#16480](https://github.com/medusajs/medusa/pull/16480))

- **Product category scalar fields** — Return all scalar fields from product category queries when no `select` is specified. ([#16493](https://github.com/medusajs/medusa/pull/16493))

- **Variant fields for inventory quantity** — Automatically select required variant fields when inventory quantity is requested; handle `null` variants gracefully. ([#16444](https://github.com/medusajs/medusa/pull/16444))

- **Order shipping method fields** — Select shipping method fields correctly when listing orders with totals. ([#16241](https://github.com/medusajs/medusa/pull/16241))

- **Credit line totals** — Preserve net negative credit line totals instead of zeroing them out. ([#16303](https://github.com/medusajs/medusa/pull/16303))

- **OAuth state cache** — Await the OAuth state cache write so the state is persisted before the redirect completes. ([#16571](https://github.com/medusajs/medusa/pull/16571))

- **Redis caching** — Fixed the Redis provider overwriting keys on `set`; await `set` and `clear` calls in the caching module; removed an unnecessary quadratic computation in the caching layer. ([#16549](https://github.com/medusajs/medusa/pull/16549), [#16547](https://github.com/medusajs/medusa/pull/16547), [#16496](https://github.com/medusajs/medusa/pull/16496))

- **`created_by` on fulfillment/shipment** — The actor ID is now correctly recorded as `created_by` when creating fulfillments and shipments. ([#16582](https://github.com/medusajs/medusa/pull/16582))

- **4xx span status** — 4xx HTTP responses no longer set the OpenTelemetry span status to `Error`, reducing noise in tracing dashboards. ([#16561](https://github.com/medusajs/medusa/pull/16561))

- **Draft order item change deletion** — Fixed inability to delete pending draft order item change actions from the dashboard. ([#16616](https://github.com/medusajs/medusa/pull/16616))

- **Create fulfillment form pagination** — Fixed select pagination issues in the create-fulfillment form. ([#16495](https://github.com/medusajs/medusa/pull/16495))

- **Duplicate query in product** — Removed a duplicate `query` call in the product module. ([#16530](https://github.com/medusajs/medusa/pull/16530))

- **Promotion campaign budget type** — Campaign budget type is now correctly included in promotion status columns. ([#16254](https://github.com/medusajs/medusa/pull/16254))

- **Isolated request allowed fields** — Request `allowedFields` are now isolated per-request so one request cannot bleed its field allowlist into another. ([#16313](https://github.com/medusajs/medusa/pull/16313))

- **Admin bundler zoom** — The generated admin HTML no longer blocks browser zoom. ([#16449](https://github.com/medusajs/medusa/pull/16449))

- **HTML `lang` attribute** — The admin dashboard now sets the correct `lang` attribute on the `<html>` element. ([#16450](https://github.com/medusajs/medusa/pull/16450))

- **Deleted stock locations in fulfillment** — The order fulfillment section now handles deleted stock locations without crashing. ([#16006](https://github.com/medusajs/medusa/pull/16006))

- **CSV export columns** — Fixed column output for order CSV exports. ([#16216](https://github.com/medusajs/medusa/pull/16216))

- **Workflow execution state column** — The workflow execution state column in the dashboard now has its own translation key to avoid conflicts. ([#16594](https://github.com/medusajs/medusa/pull/16594))

- **Loyalty plugin** — Fixed account locking on debit, gift card expiry date validation, and gift card product editing following global product-option changes. ([#16347](https://github.com/medusajs/medusa/pull/16347), [#16348](https://github.com/medusajs/medusa/pull/16348), [#16475](https://github.com/medusajs/medusa/pull/16475))

- **Test utilities** — `process.env` is now restored after test runner cleanup so environment mutations do not leak between test suites. ([#16273](https://github.com/medusajs/medusa/pull/16273))

### Translations

- Updated Croatian admin translations. ([#16675](https://github.com/medusajs/medusa/pull/16675))
- Updated Russian (`ru`) admin translations: added missing keys, plural forms (`_few`/`_many`), and fixed mistranslations. ([#16402](https://github.com/medusajs/medusa/pull/16402))
- Fixed missing Polish plural forms in dashboard translations. ([#16638](https://github.com/medusajs/medusa/pull/16638))

---

## Contributors

Thank you to all the contributors who made this release possible:

- [@NicolasGorga](https://github.com/NicolasGorga)
- [@sradevski](https://github.com/sradevski)
- [@shahednasser](https://github.com/shahednasser)
- [@lazerg](https://github.com/lazerg)
- [@luxapan](https://github.com/luxapan)
- [@lvkmsk](https://github.com/lvkmsk)
- [@leobenzol](https://github.com/leobenzol)
- [@DS123-ally](https://github.com/DS123-ally)
- [@dhruvdavest07](https://github.com/dhruvdavest07)
- [@PranshulSoni](https://github.com/PranshulSoni)
- [@peterlgh7](https://github.com/peterlgh7)
- [@shuvamk](https://github.com/shuvamk)
- [@ebrahim2355](https://github.com/ebrahim2355)
- [@Nidhi-Gahlawat](https://github.com/Nidhi-Gahlawat)
- [@HEMANTHSV31](https://github.com/HEMANTHSV31)
- [@iruzen-dono](https://github.com/iruzen-dono)
- [@yzxcj797](https://github.com/yzxcj797)