sports-retailer · medusa migration

nz dtc · ecommerce

leaving a closed e-commerce platform without losing checkout.

a growing nz dtc sports retailer needed to leave a closed e-commerce platform without losing its catalogue, its checkout, or the customer data its operators had quietly come to depend on.

the problem

the retailer had grown a few years on a closed shopify storefront. a multi-brand catalogue across two regions, in-house fulfilment from a single warehouse, a quiet but loyal customer base. the storefront worked. the platform underneath it had quietly turned into the bottleneck.

the rent line was the visible problem. monthly platform fees plus a long stack of paid apps for things that should be one thing: seo metadata, profit reporting, payment-method icons, checkout customisations, review widgets. each app charged monthly. each app owned a slice of the customer’s data. each app was one acquisition or pricing change away from being someone else’s problem.

the deeper problem was less obvious. the catalogue, the order history, the customer list, the reviews, the discount codes — all of it lived inside the platform’s data model. exporting any one of those was possible. exporting all of them, in a shape that another platform could re-import without losing line-item history or review attribution, was an engineering project the platform had no incentive to make easy. customer passwords cannot be exported at all. every migration would mean a forced password reset at cutover, no matter the destination.

prices stored as integer cents are fine until you sell across two currencies, and then they are a slow leak: rounding errors stack up across multi-currency promotions and surface in tax invoices. checkout was a black box: the team could change copy and fields, but not the order in which payment options rendered, not how 3ds modals behaved, not how apple pay verified the domain.

the obvious play would have been to replatform to another hosted store: stay inside the same vendor’s hydrogen rails, or move sideways to a competitor with the same shape. that would have replaced one set of app fees with another, kept payments locked to the platform’s processor, and left the same data ceiling intact. the team had seen that movie before. they wanted to leave the genre.

why we shaped it this way

we shortlisted three real options: stay on the current platform and squeeze costs by trimming apps; move to the same vendor’s headless tier and own the storefront only; or move to medusa v2 and own the whole stack on infrastructure the retailer chose.

option one capped the upside. apps come back. so do per-transaction fees.

option two looked clean on paper, but it kept payments, the order model, and the customer record inside the same vendor. headless changes who renders the page. it does not change who owns checkout.

we picked medusa v2. the reasons that mattered:

  • open core (mit licence). the retailer can take their backend and run it themselves the day omit stops being useful. that is an exit clause baked into the stack, not the contract.
  • prices stored as decimals, not cents. medusa v2’s pricing model handles multiple currencies natively without integer-cents workarounds, which removes a class of rounding bug we have shipped in past lives.
  • the backend self-hosts cleanly. one small server for medusa, postgres, and redis. cloudflare r2 for media, with no egress fees inside the cf network. the storefront builds static pages and ships to cloudflare workers at the edge. each layer is replaceable.
  • payments stay on the retailer’s own stripe account. we do not insert ourselves as merchant of record. we do not take connect fees. they own the relationship; we own the integration.
  • the parts medusa does not ship out of the box (a fulfilment provider for nz couriers, an admin widget for tax overrides, a profit dashboard that respects how the team actually thinks about margin) are small bounded builds, not framework rewrites.

we are not the first to choose medusa for this shape of retailer. we are choosing it knowing the gaps, and writing the fulfilment provider, the tax provider, and the admin widgets ourselves.

outcome

a dogfood instance is live: medusa v2 backend self-hosted, an astro storefront on cloudflare workers, the existing catalogue imported with images on r2, both regions and currencies wired, https end to end.

the production cutover is still ahead. payments, fulfilment, customer and order migration, the seo redirect map, and the smoke suite are scoped and in build. the retainer model is set: omit operates the stack monthly. the retailer keeps their stripe account, their domain, their customer relationships, and the right to walk with their data.

stack medusa v2 · astro 6 · cloudflare workers · r2 · hetzner
build ~8 weeks scoped, in build
operate in flight, monthly
platform model open core, mit licence
payments on the retailer's own stripe account
engagement shape fixed build, monthly operate