Highlights
Authentication improvements
This release enhances security around resetting passwords.
Updated token handling in password resets
Warning
Breaking changes: this change is not backward compatible due to security considerations
The token for updating provider identity is now accepted as a Bearer token in the authorization header instead of query parameters.
Before:
POST /auth/user/emailpass/update?token=ey...
After:
POST /auth/user/emailpass/update
// headers
{ authorization: "Bearer ey..." }
This enhances security around token management, for example, by preventing token exposure in logs.
More specifically, this will affect the reset password flow that relies on this endpoint.
Required action
- In your client (e.g. Next.js storefront), move the token from query params to the authorization header
Removed deprecated actorType
from reset password event
We have removed a long deprecated property, actorType
from the reset password event. Instead, use the actor_type
.
API Routes loader refactor
Important
While these changes sound dramatic, if you have used middleware and API Routes as intended, there will be no required actions nor breaking changes.
This release includes a refactor of our API Routes loading mechanism. The previous loader had long-standing issues that required hacky workarounds. To resolve these issues, weβve rebuilt the loader from scratch to ensure predictable behavior in how routes and middleware are registered within the application.
The root of the issues with the old implementation was mainly related to the following decisions:
-
Plugins, Medusa core, and the application used isolated router instances of Express. This means global middleware and error handlers are isolated, too. If you define an error handler in your app, it won't be able to handle errors from the core routes.
-
Middlewares defined inside the
src/api/middlewares.ts
file were always defined before the filesystem routes, and this led to unexpected behavior in many scenarios. For example, the middleware handler registered for routeadmin/payments/payment-providers
will also run when a request comes in for/admin/payments/:id
, even though there is a static route in place for the/admin/payments/payment-providers
URI. -
There was no clarity on the ordering in which routes and middleware were executed. With the new implementation, this has been fixed (more on ordering is discussed below).
-
Overriding an existing route defines multiple express handlers under the hood. It worked previously, because the route handlers never call
next
, hence the previous implementation was never executed. With the newer implementation, the route overrides from a Medusa application will be registered over the routes from the core or the plugins.
To resolve these issues, weβve rebuilt the loader from scratch to ensure predictable behavior in how routes and middleware are registered within the application. Additionally, the old API Routes loader unintentionally allowed overriding core API routes and middleware. While this behavior remains for now, it will be removed in a future release. Please prepare your application based on the behavior described in the following sections.
Global middleware
A global middleware is defined using the defineMiddlewares
function without specifying an HTTP method.
defineMiddlewares([
{
matcher: '/admin',
middleware: [
(req, res, next) => console.log("Hi from global middleware")
],
}
])
The order of registering global middleware is as follows:
- Global middleware from the core
- Global middleware from plugins (in the order plugins are registered)
- Global middleware from the app
Route middleware
A route middleware is also defined using the defineMiddlewares
method. However, they must always specify the HTTP method(s) they want to target.
defineMiddlewares([
{
matcher: '/admin/*',
methods: ["GET", "POST"],
middleware: [
(req, res, next) => console.log("Hi from route middleware")
],
}
])
The order of registering route middleware is as follows:
- Route middleware from the core
- Route middleware from plugins (in the order plugins are registered)
- Route middleware from the app
API Routes
An API Route is always attached to a method, decided by the exported const from the route file. API Routes are registered by scanning the filesystem.
Two types of API Routes are supported:
- Static segments:
/admin/products/route.ts
- Dynamic segments:
/admin/products/[id]/route.ts
Sorting routes and middleware
Before registering routes + middleware with Express, they are sorted as described below:
- Global middleware
- Route middleware
- API Routes
On top of this, each segment from the API Route matcher is sorted by its specificity:
- Wildcard segments, e.g.
/admin/products/*
- Regex segments, e.g.
/admin/(product-types|product-collections)
- Static segments, e.g.
/admin/products
- Dynamic segments, e.g.
/admin/products/:id
Important note about middleware
Middleware never overrides any existing middleware; they are added to the stack of existing middleware. For example, if you define a custom validation middleware on an existing route, then both the original and the custom validation middleware will run.
Performance improvements
This release contains general and, in some areas, significant performance improvements. Over the past weeks, we have focused on removing performance bottlenecks in Medusa. While many of the improvements impacted the entire application, the most significant ones are for cart operations.
Here's a summary of the changes and improvements:
Application-level changes
- Eliminated unnecessary module calls in cart workflows to ensure we don't create unused transactions and have redundant database communication
- Minimized redundant data fetching in cart workflows, ensuring that only the data that is needed for the operation is fetched
- Changed workflows to accept prefetched data instead of always fetching the data independently
- Introduced finer control over cart refresh logic to ensure we don't perform unnecessary cart refreshes
- Replaced the Redis
del
operation with the non-blocking deleteunlink
to reduce the load on Redis - Replaced inefficient soft and hard deletes by performing the operation in a single query instead of multiple
- (Dashboard) Minimized redundant data fetching for dashboard pages
Database-level changes
These changes are a result of thorough testing of our own infrastructure. Therefore, they should be deemed a recommendation rather than an absolute configuration. Additionally, it is important to highlight that these tests were run against a Neon Database with a PgBouncer setup. Without PgBouncer and Neon, the configuration would likely look different.
// medusa-config.ts
export default defineConfig({
projectConfig: {
// ...other options
databaseDriverOptions: {
pool: {
max: 8, // Fixed pool size of 8
idleTimeoutMillis: 30000, // Close idle connections after 30 seconds
connectionTimeoutMillis: 10000, // Fail on connection attempt after 10 seconds
createRetryIntervalMillis: 100 // Time between connection retries
},
},
...
},
}
Results
We have run two tests:
Test 1:
- Operation: Add to cart
- Database spec:
- 0.25 CPU
- 1 GB RAM
- Server spec:
- 1 CPU
- 2 GB RAM
- Load: 5 carts, operations for each cart run in parallel; for each cart, we add 100 items sequentially
p95: 1180ms -> 866ms
p50: 833ms -> 593ms
Test 2:
- Operation: Add to cart
- Database spec:
- 0.5 CPU
- 2 GB RAM
- Server spec:
- 1 CPU
- 2 GB RAM
- Load: 5 carts, operations for each cart run in parallel; for each cart, we add 100 items sequentially
p95: 1180ms -> 773ms
p50: 849 -> 537ms
Both tests show an approximate performance improvement of 30-35%.
Features
- feat: add middleware and routes sorter by @thetutlage in #11526
- feat(core-flows,types,cart): add credit lines to cart by @riqwan in #11419
- feat: add routes loader by @thetutlage in #11592
- feat: add middleware-file-loader by @thetutlage in #11638
- feat(utils): add error message when manager is not found in context by @riqwan in #11693
- feat: Replace existing router with the new implementation by @thetutlage in #11646
Bugs
- fix(types, medusa): remove fulfillment and payment status filters from validator + http types by @shahednasser in #11604
- fix(types): Allow providing either vite@5 or vite@6 for the peer dependency by @kasperkristensen in #11581
- fix(promotion): scope uniqueness index to non deleted promotions by @riqwan in #11624
- fix(core-flows): support 0 as a valid unit price for custom line items by @riqwan in #11631
- fix(core-flows): unsafe access to a variant property by @fPolic in #11588
- fix(medusa): deleting location level by @fPolic in #11630
- fix: Exclude the health endpoint from being logged by @sradevski in #11650
- fix(dashboard): display CG in PL configuration details by @fPolic in #11653
- fix(dashboard): few admin bug fixes by @fPolic in #11655
- fix(core-flows): return created order return from workflow by @fPolic in #11642
- fix(dashboard): filter outbound shipping options in RMA by @fPolic in #11629
- fix(dashboard): variant inventory section by @fPolic in #11669
- fix(dashboard): bust variant inventory cache on inventory update by @riqwan in #11690
- fix(types,medusa): calculate taxes for original price by @riqwan in #11692
- fix(dashboard): Use derived state in DataTable by @kasperkristensen in #11487
- fix(dashboard, core-flows, medusa): prevent creation of empty fulfillments by @fPolic in #11664
- fix(order): summary raw totals by @carlos-r-l-rodrigues in #11712
Documentation
- docs: fix start command in deployment guides by @shahednasser in #11571
- docs: updates for 2.5.1 release by @shahednasser in #11574
- docs: fix OAS generator + generate OAS by @shahednasser in #11584
- docs: fix to recipe after latest update by @shahednasser in #11586
- docs: document different ways of selling products by @shahednasser in #11446
- docs: document swc dependency for plugins by @shahednasser in #11426
- docs: fix incorrect link in tax-inclusive-pricing by @shahednasser in #11599
- docs: fix types for provider docs not generated correctly by @shahednasser in #11598
- docs: finish user guide updates by @shahednasser in #11603
- docs: prepare projects configurations for user guide by @shahednasser in #11610
- docs: add user guide to navbar by @shahednasser in #11611
- docs: add ERP recipe + Odoo guide by @shahednasser in #11594
- docs: add user guide tags + sidebar links for commerce modules by @shahednasser in #11612
- docs: fix small typo by @shahednasser in #11616
- docs: add new cards to recipe section by @shahednasser in #11615
- docs: add notes + missing links for user guide by @shahednasser in #11621
- docs: updates to tax provider and other provider guides by @shahednasser in #11626
- docs: change question in user guide feedback form by @shahednasser in #11628
- docs: fix meta assets of user guide by @shahednasser in #11639
- docs: update subscriptions recipe with stripe payment by @shahednasser in #11651
- docs: typo on homePage codeTabs notificationModuleService by @Rkallenkoot in #11658
- docs: move edit date to footer by @shahednasser in #11667
- docs: fix weird scroll behavior by @shahednasser in #11668
- docs: fix examples in workflows reference by @shahednasser in #11688
- docs: add section on row click in DataTable by @shahednasser in #11694
- docs: add more clarification to reset password in API reference by @shahednasser in #11695
- docs: Added a missing "/" by @Sai-Santhan-Dodda in #11696
- docs: fix Sanity example by @ismailsabet in #11697
- docs: open prerequisite links in new tab by @shahednasser in #11701
- docs: update OAS of reset password routes by @shahednasser in #11711
Chores
- chore(ui): explicitely specify the props type of InlineTip by @shahednasser in #11575
- chore: update TSDocs of account holder additions by @shahednasser in #11576
- chore(types): change payment provider types to interfaces by @shahednasser in #11597
- chore(ui): updates to InlineTip's TSDocs by @shahednasser in #11580
- chore(): Improve internal repository delete algo by @adrien2p in #11601
- chore(core-flows): reserve inventory from available location by @carlos-r-l-rodrigues in #11538
- chore(product): revamp upsertWithReplace and Remove its usage from product creation by @adrien2p in #11585
- chore(core-utils): avoid overfetching to refresh cart by @carlos-r-l-rodrigues in #11602
- chore(core-flows): pass cart as reference to subflows by @carlos-r-l-rodrigues in #11617
- chore(core-flows): use cart id when force_refresh is true by @carlos-r-l-rodrigues in #11625
- chore(types): improvements to tax provider tsdocs by @shahednasser in #11627
- chore: Move token from params to body by @olivermrbl in #11281
- chore: prevent workflow steps to call modules when not necessary by @adrien2p in #11632
- chore(): Improve cascade soft deletetion/restoration and update by @adrien2p in #11618
- chore(): Add support for extra pool configuration by @adrien2p in #11636
- chore(): Prevent sub workflow events release early + redis unlink by @adrien2p in #11641
- Chore/link module serialization by @adrien2p in #11643
- chore(): Improve cart update line items by @adrien2p in #11666
- chore(workflows-sdk): fix examples in TSDocs by @shahednasser in #11689
- chore(framework): Unified resource loading and exclude non js/ts files by @adrien2p in #11707
Other Changes
- Fix/add additionl data to product categories hook by @bouazzaayyoub in #11226
- Add missing translations & change some badly translated by @Furman1331 in #11670
- feat: added hook for createStockLocationsWorkflow by @Avia-Code in #11491
New Contributors
- @bouazzaayyoub made their first contribution in #11226
- @Rkallenkoot made their first contribution in #11658
- @Sai-Santhan-Dodda made their first contribution in #11696
- @ismailsabet made their first contribution in #11697
Full Changelog: v2.5.1...v2.6.0