v2.20.0: Security Fixes, Performance Improvements, Calculated Shipping Options in Draft Orders
LatestHighlights
Medusa MCP users can update their project using the following prompt:
Update my Medusa project to v2.20.0Payment 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/productsGET /store/products/:id
To change the limit for every Store route, set storeRelationsLimit in your HTTP configuration:
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:
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-redisprovider.
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:migrateafter upgrading. (#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; rundb:migrateto create them. (#16545) -
searchManymoved to the provider interface — ThesearchManymethod has been pushed down to the search provider interface so each provider can optimize batch queries internally. Custom search providers must implementsearchMany. (#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)
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, #16673, #16676)
-
ESLint plugin: new workflow rules — Three new rules for
@medusajs/eslint-plugin:no-nested-when-thencatches nestedwhen/thenblocks that silently do not compose correctly;missing-when-namerequires allwhencalls to have anameargument for better traceability;throw-in-workflow-definitiondisallowsthrowstatements in workflow definition functions, which must useMedusaErrorin steps instead. (#16520, #16585, #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)
-
Store order routes field expansion — Blocked field-expansion pivot paths on store order routes to prevent information leakage. (#16480)
-
Product category scalar fields — Return all scalar fields from product category queries when no
selectis specified. (#16493) -
Variant fields for inventory quantity — Automatically select required variant fields when inventory quantity is requested; handle
nullvariants gracefully. (#16444) -
Order shipping method fields — Select shipping method fields correctly when listing orders with totals. (#16241)
-
Credit line totals — Preserve net negative credit line totals instead of zeroing them out. (#16303)
-
OAuth state cache — Await the OAuth state cache write so the state is persisted before the redirect completes. (#16571)
-
Redis caching — Fixed the Redis provider overwriting keys on
set; awaitsetandclearcalls in the caching module; removed an unnecessary quadratic computation in the caching layer. (#16549, #16547, #16496) -
created_byon fulfillment/shipment — The actor ID is now correctly recorded ascreated_bywhen creating fulfillments and shipments. (#16582) -
4xx span status — 4xx HTTP responses no longer set the OpenTelemetry span status to
Error, reducing noise in tracing dashboards. (#16561) -
Draft order item change deletion — Fixed inability to delete pending draft order item change actions from the dashboard. (#16616)
-
Create fulfillment form pagination — Fixed select pagination issues in the create-fulfillment form. (#16495)
-
Duplicate query in product — Removed a duplicate
querycall in the product module. (#16530) -
Promotion campaign budget type — Campaign budget type is now correctly included in promotion status columns. (#16254)
-
Isolated request allowed fields — Request
allowedFieldsare now isolated per-request so one request cannot bleed its field allowlist into another. (#16313) -
Admin bundler zoom — The generated admin HTML no longer blocks browser zoom. (#16449)
-
HTML
langattribute — The admin dashboard now sets the correctlangattribute on the<html>element. (#16450) -
Deleted stock locations in fulfillment — The order fulfillment section now handles deleted stock locations without crashing. (#16006)
-
CSV export columns — Fixed column output for order CSV exports. (#16216)
-
Workflow execution state column — The workflow execution state column in the dashboard now has its own translation key to avoid conflicts. (#16594)
-
Loyalty plugin — Fixed account locking on debit, gift card expiry date validation, and gift card product editing following global product-option changes. (#16347, #16348, #16475)
-
Test utilities —
process.envis now restored after test runner cleanup so environment mutations do not leak between test suites. (#16273)
Translations
- Updated Croatian admin translations. (#16675)
- Updated Russian (
ru) admin translations: added missing keys, plural forms (_few/_many), and fixed mistranslations. (#16402) - Fixed missing Polish plural forms in dashboard translations. (#16638)
Contributors
Thank you to all the contributors who made this release possible: