Production bugs don’t care that your infrastructure costs €3.29/mo.
Medusa v2 is genuinely good — the headless model, the workflow engine, the v2 admin API are all a step up from v1. But the docs surface 80% of what you’ll hit, and the remaining 20% is where weekends go. These are three bugs I hit running nadiapoe.co.uk‘s shop on Medusa v2. All three had simple fixes. None of the fixes were obvious.
Bug 1: The Date.parse shipping rule trap
The shop has two delivery options — a small incentive to encourage slightly larger print orders:
- Standard delivery — £4.50, always available.
-
Free delivery — £0, available when
cart_subtotal >= 60.
A £100 cart. Free Delivery disappears. Standard gets auto-selected. Refresh, clear cart, retry — same result. Stranger: a £10 test cart shows Free Delivery correctly. The rule is set up right. The data in the database looks right. Something in the middle is broken.
That something is buried in @medusajs/fulfillment/dist/utils/index.js. The comparator for gte / lt rules does this:
const left = Date.parse(a)
const right = Date.parse(b)
if (!isNaN(left) && !isNaN(right)) {
return new Date(left) < new Date(right) // date branch
}
return Number(a) < Number(b) // numeric branch
That looks fine until you remember what Date.parse does to bare integer strings:
Date.parse("60") // → -315619200000 (year 1960 — 2-digit year expansion)
Date.parse("100") // → -59011459125000 (year 100 AD)
Date.parse("60.00") // → NaN
A £100 cart with cart_subtotal = "100" and a rule value of "60" evaluates as Date(100 AD) < Date(1960) → true. The "show this option when subtotal is below £60" condition fires on a cart worth nearly twice the threshold. Free Delivery gets stripped.
The fix is one toFixed(2). From api/src/workflows/shipping-options-context.ts:
import {
listShippingOptionsForCartWorkflow,
listShippingOptionsForCartWithPricingWorkflow,
} from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
const injectCartSubtotal = async ({ cart }: { cart: any }) =>
new StepResponse({ cart_subtotal: (cart.item_total ?? 0).toFixed(2) })
listShippingOptionsForCartWorkflow.hooks.setShippingOptionsContext(injectCartSubtotal)
listShippingOptionsForCartWithPricingWorkflow.hooks.setShippingOptionsContext(injectCartSubtotal)
The rule values stored in the database have to match the same shape — "60.00", not "60". Once both sides are decimals, Date.parse returns NaN and the evaluator falls through to the numeric branch.
Bug 2: There are two shipping workflows, and you have to hook both
Look at the snippet above again. There are two workflow hooks, not one. That's not an accident.
By default Medusa v2's shipping rule evaluator only sees is_return and enabled_in_store. Anything cart-financial — cart_subtotal, item_count, currency, region — has to be injected via the setShippingOptionsContext hook. That part is documented.
What isn't documented: there are two workflows that evaluate shipping options.
-
listShippingOptionsForCartWorkflowruns when the storefront fetches options to display. -
listShippingOptionsForCartWithPricingWorkflowruns insideaddShippingMethodToCartWorkflowwhen a customer actually selects an option.
Hook only the first and the storefront shows Free Delivery correctly. The customer selects it, gets a 400 back — because the second workflow has no cart_subtotal in its context, the rule fails, and Medusa rejects the assignment as "option not available for this cart." The response body is generic. The server logs are silent.
Cost: one Friday evening.
Bug 3: The workaround that aged badly
Some workarounds need an expiry date. This one didn't have one.
Early Medusa v2 had two related bugs — #11766 and #13301. Stripe charged the card, webhooks fired, but order.paid_total stayed at 0 and the payment status never advanced past pending. Every order had to be manually marked paid in the admin — which also meant no order confirmation email fired, since the email subscriber was gated on a completed order. Customers paid, heard nothing, and had to be chased manually.
I patched it with a subscriber on order.placed that called capturePaymentWorkflow directly:
// api/src/subscribers/order-capture-payment.ts (DELETED in 2026-05)
export default async function orderCapturePayment({ event, container }) {
// ...fetch payment collection, mark fully captured, refresh order
}
It worked. Shipped.
Then Medusa v2.11.1 landed and fixed both upstream bugs. The subscriber kept running — double-firing the capture (harmless, since the payment was already captured) but also re-emitting order.placed. That re-emission triggered the email subscriber a second time. Every customer started getting two identical confirmation emails.
I didn't notice until v2.14.2, months later, when an unrelated change made the second order.placed event log a warning. The fix was a one-line deletion. The lesson cost more than the bug did.
Every workaround for a third-party bug needs three things:
- The upstream issue URL in a comment — so future-you knows why it exists.
- A version check or feature flag to disable it — so it can be switched off without deleting it immediately.
- A note to revisit on the next major upgrade of that dependency.
I had #1. I didn't have #2 or #3. The duplicate emails probably did 30 minutes of brand damage before I caught it. The fix was free. The discipline to set expiry dates on workarounds isn't instinctive, but it's cheap.
One note if you're starting a Medusa v2 project: the v1 docs URL still resolves and Google ranks both. Make sure the URL doesn't have /v1/ in it before you copy a snippet — the APIs changed significantly between versions.
Live shop: nadiapoe.co.uk.