The technically hard parts of a Shopify migration are not the ones teams plan for. Products import in an afternoon. What breaks timelines is subscriptions, historical orders, and the four hundred URLs that used to rank.
This is the plan we actually run.
Phase 0: export everything
Two sources. The Shopify admin exports products, customers and orders as CSV, which is fine for a first look. For a real migration use the Admin API, because CSV drops metafields, variant-level inventory detail and most order sub-records.
curl -sS "https://your-store.myshopify.com/admin/api/2024-10/products.json?limit=250" \
-H "X-Shopify-Access-Token: $SHOPIFY_TOKEN" \
-o products.jsonPaginate with the Link header until it stops offering rel="next". Pull products, variants, collections, customers, orders, discounts and metafields into raw JSON, and commit that dump somewhere safe. Every later step reads from these files, never from the live store — reproducibility matters when you run the import eleven times.
Phase 1: map the data model
Most of it is a direct translation.
| Shopify | Medusa | Notes |
|---|---|---|
| Product | Product | Direct |
| Variant | Product Variant | Shopify caps options at 3; Medusa does not |
| Collection (manual) | Product Collection | Direct |
| Collection (smart) | Category or query | Rules do not migrate; re-express them |
| Metafield | Module field or metadata | The decision point — see below |
| Customer | Customer | Passwords excluded |
| Order | Order | Import as records, not replayed checkouts |
| Discount / Price rule | Promotion | Rule syntax differs |
| Market | Region | Regions carry currency and tax |
| Location | Stock Location | Direct |
Metafields deserve real thought. Shopify stores make metafields do everything: spec sheets, badge flags, related products, compliance data. In Medusa you have two options. Low-value display data goes in metadata on the record. Anything you query, validate or build logic against should become a proper field on a custom module with a link to the product. Dumping everything into metadata is the migration decision teams regret at month six.
Phase 2: import the catalog
Write the importer as a Medusa workflow rather than a standalone script, so it runs inside the app with the container available and rolls back cleanly on failure.
import { createWorkflow, WorkflowResponse, transform } from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
type ShopifyProduct = {
handle: string
title: string
body_html: string
variants: { sku: string; price: string; option1: string | null }[]
options: { name: string; values: string[] }[]
}
export const importShopifyProducts = createWorkflow(
"import-shopify-products",
(input: { products: ShopifyProduct[] }) => {
const payload = transform({ input }, ({ input }) => ({
products: input.products.map((p) => ({
title: p.title,
handle: p.handle,
description: p.body_html,
options: p.options.map((o) => ({ title: o.name, values: o.values })),
variants: p.variants.map((v) => ({
title: v.option1 ?? p.title,
sku: v.sku,
prices: [{ amount: Math.round(parseFloat(v.price) * 100), currency_code: "usd" }],
})),
})),
}))
const created = createProductsWorkflow.runAsStep({ input: payload })
return new WorkflowResponse(created)
}
)Two rules that save days. Keep the Shopify handle as the Medusa handle — your redirect map depends on it. And batch in chunks of 50 to 100; a 5,000-product single transaction will time out and tell you nothing useful about which record broke.
Phase 3: customers
Import identity and addresses. You cannot import passwords: Shopify stores hashes you never see, and even if you could, they use a different algorithm.
The migration-day flow that works is a forced reset framed as a security upgrade — an email to the full list on cutover day, plus a login page that detects a migrated account with no credential and offers a one-click reset link. Do not silently fail the login; that is where support tickets come from.
Preserve created_at on import. Customer tenure drives segmentation, loyalty tiers and lifetime-value reporting, and it is trivially easy to lose.
Phase 4: historical orders
Import orders as records. Do not push them through the order workflow — you will trigger inventory reservations, payment authorisations and confirmation emails for purchases made two years ago.
Decide the depth deliberately. Two years covers returns windows, support lookups and cohort analysis. Full history is nice for reporting but multiplies import time and rarely earns it. Whatever you choose, keep the Shopify order number in metadata so support can search by the number the customer has in their inbox.
Phase 5: subscriptions
This is the phase that slips. Shopify subscription apps store payment tokens bound to a specific gateway and merchant account. Those tokens are usually not portable, which means:
- Ask your PSP whether tokens can be migrated between merchant accounts. Sometimes yes, on the same processor. Usually no.
- If no, subscribers must re-enter payment details. Expect to lose some.
- Sequence it: run both systems in parallel, migrate cohorts, communicate early, and give people a real incentive to re-authorise.
Build the subscription logic itself in Medusa before migrating anyone — see subscriptions in Medusa. Never cut over subscribers to a system you have not billed a real cycle on.
Phase 6: the redirect map
The highest-ROI hour in the whole project.
| Shopify URL | Medusa / storefront URL |
|---|---|
| `/products/<handle>` | `/products/<handle>` |
| `/collections/<handle>` | `/collections/<handle>` |
| `/collections/<c>/products/<p>` | `/products/<p>` |
| `/pages/<handle>` | `/<handle>` |
| `/blogs/news/<handle>` | `/blog/<handle>` |
Keeping the handle makes most of this an identity mapping, which is exactly why you kept it. Then:
- Export every ranking URL from Search Console, not just the ones in your sitemap. Old collection filters and paginated URLs accumulate links.
- Implement 301s in
next.config.jsor at the edge — not in application code, which adds latency to every request. - Verify the whole map against the live site before DNS changes, then again after.
Storefront SEO for Medusa covers metadata, structured data and sitemaps for the new build.
Phase 7: cutover
A checklist we run, in order:
- Freeze catalog edits on Shopify.
- Final delta import — products, customers and orders changed since the last run.
- Smoke test the full purchase path on production infrastructure with a real card.
- Verify tax, shipping rates and inventory counts against the old store.
- Switch DNS during your lowest-traffic window.
- Submit the new sitemap; watch Search Console coverage daily for two weeks.
- Keep Shopify on the lowest plan for 30 days as a read-only reference.
Expect a two-to-four-week ranking dip. That is normal. What is not normal is a dip that does not recover — that is always redirects.
We run these migrations end to end, including the parts nobody enjoys. Get in touch if you want the plan applied to your catalog.
Frequently asked questions
How long does a Shopify to Medusa migration take?
Six to ten weeks for a mid-size store. Roughly: one week of export and mapping, two to three weeks of storefront build, two weeks of data import and validation, one to two weeks of payment and fulfilment integration, and a week of testing and cutover. Subscriptions or heavy B2B logic add three to four weeks.
Will I lose my Google rankings?
Not if you redirect properly. Every URL that receives traffic or links needs a 301 to its new equivalent, in place on launch day. Typical behaviour is a dip for two to four weeks followed by recovery, and stores that improve site speed in the rebuild often end up ahead.
Can I migrate Shopify customer passwords?
No. Shopify does not expose password hashes and uses a different hashing scheme. Plan a forced reset communicated as a security upgrade, with a login page that recognises migrated accounts and offers a one-click reset.
What happens to my Shopify apps?
They do not come with you. Audit them before migrating and sort them into three buckets: replaced by Medusa core functionality, replaced by a direct third-party integration, and genuinely needed as custom development. Most stores find half their apps were compensating for platform limitations that do not exist in Medusa.
Should I migrate historical orders?
Import at least two years, as records rather than replayed checkouts, to cover returns, support lookups and cohort analysis. Keep the original Shopify order number in metadata so support can find orders by the number customers actually have.
Can I run Shopify and Medusa in parallel?
Yes, and for subscriptions you often must. Run Medusa on a subdomain, migrate a cohort, and validate before the full switch. Keep one system authoritative for inventory during any overlap, or you will oversell.
The US Vape Crackdown and Shopify: Why Brands Are Migrating to Medusa
Shopify Payments prohibits nicotine. The PACT Act killed your shipping options. Here is what actually happens when a vape brand gets deplatformed — and the migration path off Shopify onto self-hosted Medusa.
WooCommerce to Medusa Migration: Escaping post_meta
WooCommerce data lives in WordPress's generic post tables. Extracting it cleanly, mapping variable products, and deciding what to do with thirty plugins.
The Real Cost of Headless Commerce: A TCO Model You Can Argue With
Platform fees are the visible cost and rarely the largest. A three-year model comparing hosted SaaS against self-hosted Medusa, including the lines people leave out.



