Guides · July 23, 2023
Medusa.js in Production: What We Learned Running It for Real
Medusa.js holds up in production as a headless commerce engine once you accept its module-and-workflow architecture on its own terms — the rough edges show up in migrations, admin customization, and search, not in checkout or order correctness.
By Polo Themes
Medusa.js holds up well in production once a team stops treating it like a Shopify replacement and starts treating it like what it actually is: a set of composable commerce modules and workflows running on Node, MikroORM, and Postgres. The framework's core transaction paths — cart, checkout, payment capture, order state — are solid and well tested. The friction shows up around the edges: migration discipline across custom modules, admin UI customization once you go past the provided widgets, and the fact that anything resembling merchandising intelligence (search relevance, recommendations, personalization) is left entirely to you or a third-party integration. This is a field report on where that friction actually lives, not a marketing pass.
Why We're Writing This
Polo Themes has spent years building Shopify themes and Figma UI kits, which means we spend a lot of time in other people's commerce platforms, reading their constraints and their trade-offs. Medusa keeps coming up in conversations with developers and technical founders who want commerce logic they can actually own — self-hosted, TypeScript-native, no theme-engine sandbox, no forced app-store tax. That interest is also where our own roadmap is headed: alongside Figma kits and Shopify themes, we're building toward Next.js-native, AI-assisted design-to-code assets for exactly this kind of headless stack. So this review is written from the angle of "what would we tell a team that's about to commit to Medusa for a real store," based on how the framework actually behaves once migrations, custom modules, and real order volume enter the picture.
What Medusa Actually Is
Medusa v2 is a modular commerce framework, not a monolithic e-commerce app. The core ships as a set of independently versioned modules — product, cart, order, payment, fulfillment, pricing, promotion, customer, and more — each with its own data model, its own migrations, and a defined public API. Business logic is expressed as workflows: durable, step-based orchestrations built on Medusa's own workflow engine, which gives you retries, compensation (rollback) steps, and idempotency without reaching for an external queue for most cases. On top of that sits the Admin dashboard (a React app you can extend with widgets and routes) and the Store API, which any storefront — Next.js, Remix, or otherwise — consumes as a normal REST/JSON backend.
The framing that matters in production: Medusa is closer to "Rails for commerce" than "Shopify you can host yourself." You get real database access, real migrations, and the ability to write custom modules that live alongside the core ones. That power is exactly why it needs more engineering discipline than a hosted platform — there is no platform team catching your mistakes for you.
Where It Held Up Well
Checkout and order integrity
The cart-to-order pipeline is the part of Medusa that has clearly had the most engineering attention. Line-item pricing, tax calculation hooks, promotion application, and payment session handling all run through well-defined workflow steps with compensation logic, so a failure partway through checkout (a payment provider timeout, a fulfillment step rejecting an address) rolls back cleanly instead of leaving a store in a half-committed state. In practice, this is the area where we've seen the fewest surprises — order totals reconcile, webhook-driven payment status updates land where they should, and the workflow retry model means transient provider failures don't require manual database surgery to fix.
The module boundary is real, not decorative
A common failure mode in custom-built commerce backends is that "modules" are really just folders — everything still reaches into everything else's tables directly. Medusa's module isolation is enforced at the data-access layer: each module owns its own schema and exposes a service interface, and cross-module reads go through the query layer (Query.graph) rather than raw joins. This makes it realistic to swap or extend a single module — pricing, for instance — without destabilizing order or fulfillment. For a self-hosted platform meant to run for years under different engineering teams, that boundary is worth more than it looks on day one.
Workflows as a debugging surface
Because business processes are workflows rather than ad hoc service calls, they show up in logs and in the Admin as named, step-level executions. When something goes wrong in production — an order stuck between "payment captured" and "fulfillment created," say — you can trace exactly which step failed and whether its compensation ran, instead of grepping through generic application logs trying to reconstruct what happened. This alone has saved real debugging time on stores we've supported.
Where the Friction Actually Lives
Migration discipline is entirely on you
Medusa uses MikroORM, and MikroORM tracks executed migrations by class name, not file path or timestamp. If your team writes a custom migration in one module and, months later, someone else writes another migration in a different module that happens to generate the same class name, MikroORM will silently mark the second migration "executed" without ever running its SQL. No error, no warning — just a database that's quietly missing a column or table that your code assumes exists. This is not a hypothetical; it is the single most consequential operational gotcha we've run into with Medusa, and it only shows up once a codebase has enough custom modules that timestamp collisions become plausible. The mitigation is process, not code: enforce globally unique migration class names across every custom module, prefer generating migrations from live database state rather than hand-writing timestamps, and verify what actually executed against information_schema rather than trusting the CLI's summary output.
Admin customization has a ceiling
Medusa's Admin dashboard extension model — widgets that inject into existing pages, and custom UI routes — covers a large share of real customization needs (adding a field to the product page, a custom order tab, a settings screen for a custom module). It does not give you free rein over the existing screens themselves. Deep changes to core Admin flows — reworking the order list, changing how variants are presented — mean either forking the Admin package or living with the widget model's boundaries. Teams that expect Shopify-style unlimited theme/app customization of the backend UI should recalibrate: Medusa's Admin is meant to be extended at defined seams, not rewritten wholesale.
Search, merchandising, and personalization are DIY
Out of the box, Medusa gives you correct, well-modeled product and pricing data — it does not give you search relevance, faceted filtering at scale, or recommendation logic. Most production Medusa stores end up wiring in Meilisearch, Algolia, or Typesense for search, and building recommendation/personalization as a separate service or third-party integration. This isn't a defect so much as a scope decision consistent with the "modular framework, not full platform" philosophy — but teams that assume search will "just work" the way it does on a hosted platform are in for a surprise mid-project, and should budget for it up front rather than discovering it during a launch-readiness review.
Operational maturity requires real DevOps investment
Because Medusa is self-hosted, you inherit responsibility for the things a hosted platform normally absorbs: database backups and point-in-time recovery, connection pooling under load, worker/queue scaling for background jobs, zero-downtime deploys around schema migrations, and monitoring for the workflow engine specifically (not just generic uptime checks). None of this is unique to Medusa — it's the standard cost of self-hosting any real backend — but teams coming from Shopify or BigCommerce sometimes underestimate it because they've never had to think about it before. Budget real DevOps time, not just development time, before committing.
The Migration-Safety Checklist We'd Give Any Team
If you take one operational habit from this review, take this one — it is the highest-leverage guard against the single worst failure mode we've seen in real Medusa deployments.
- Treat migration class names as a globally unique namespace across every custom module in the codebase, not per-module.
- Prefer generating migrations from live database state over hand-writing timestamps — it removes the collision risk at the source.
- Run an automated check for class-name and filename collisions as part of CI and pre-commit, not as a manual review step someone will eventually skip.
- Never run a bare migrate command against a database that a dev server or another process currently holds open — race conditions here are the suspected cause of migrations being marked executed without their SQL actually running.
- After any migration, verify the result against information_schema directly. Do not trust a CLI's "Migrated / Skipped" summary as the source of truth.
Where Medusa Fits Next to Shopify and Headless Alternatives
Shopify remains the right default for merchants who want commerce infrastructure to be someone else's operational problem — payments, PCI compliance, uptime, and a mature theme and app ecosystem come bundled in, and our own Shopify theme catalog exists because that trade-off is the right one for a large share of stores. Medusa is the right choice for the opposite profile: teams with real engineering capacity who need custom logic that doesn't fit a theme-and-app model, who want the data model and checkout flow under their own control, and who are comfortable owning the operational surface that comes with self-hosting. It sits in a similar conceptual space to Saleor and Vendure — modular, code-first, TypeScript- or GraphQL-native — and the deciding factor between them is usually less about raw capability and more about which architecture (workflows vs. GraphQL-first vs. event-driven) matches how your team already thinks.
The pairing that shows up most often in the wild is Medusa as the commerce backend with a Next.js storefront in front of it, since Medusa's Store API is a plain REST/JSON contract with no opinion about your frontend framework. If you're evaluating that combination, it's worth being honest that the frontend layer is where most of the remaining design and build work lives once the backend is stable — product page layout, cart and checkout UX, and merchandising surfaces still need to be designed and built well, whichever commerce engine sits behind them.
Frequently Asked Questions
Is Medusa.js production-ready?
Yes, for teams with the engineering capacity to own a self-hosted Node/Postgres backend. The core commerce logic — cart, checkout, orders, payments — is solid and workflow-driven with proper rollback behavior. The gaps are operational (migrations, DevOps maturity) and feature-scope (search, personalization), not correctness bugs in the checkout path itself.
What's the biggest operational risk with Medusa in production?
Migration class-name collisions across custom modules. Because MikroORM identifies migrations by class name rather than file path, two migrations with the same generated class name in different modules can cause the second one to be silently skipped, with no error surfaced. Enforce unique names and verify migrations against information_schema rather than trusting CLI output alone.
Does Medusa include search and recommendations out of the box?
No. Medusa models product and pricing data correctly but leaves search relevance, faceting at scale, and recommendation/personalization logic to third-party integrations (Meilisearch, Algolia, Typesense, or a custom service). Budget for this as a separate workstream rather than assuming it ships with the framework.
How does Medusa compare to building headless on Shopify?
Shopify's headless path (Storefront API / Hydrogen) gives you a managed, PCI-compliant backend with a mature app ecosystem, at the cost of working within Shopify's data model and infrastructure. Medusa gives you full control over the data model, custom business logic, and hosting, at the cost of owning migrations, scaling, and operational maturity yourself. Teams choosing between them should weigh engineering capacity and how far their commerce logic needs to diverge from a standard model, more than raw feature checklists.
Should a design-focused team consider Medusa, or is it developer-only?
Medusa's backend is developer-owned by nature, but the frontend layer in front of it is exactly where design work matters most — a correct backend with a poorly designed storefront still converts poorly. Teams evaluating a Medusa build should plan for real product-page, cart, and checkout design work alongside the backend implementation, not treat it as an afterthought once the API is wired up.