WooCommerce stores commerce data in WordPress's generic content tables. Products are posts, variants are child posts, and almost every attribute is a row in wp_postmeta. It works, and it means migration starts with archaeology rather than mapping.
The upside: everything is in a MySQL database you control, so extraction is a query rather than a rate-limited API crawl.
Extraction
The REST API gives you correctly assembled objects:
curl -sS "https://yourstore.com/wp-json/wc/v3/products?per_page=100&page=1" \
-u "$WC_KEY:$WC_SECRET" -o products-1.jsonDirect SQL gives you everything, including meta the API omits:
SELECT
p.ID,
p.post_title,
p.post_name AS slug,
p.post_content AS description,
p.post_excerpt AS short_description,
p.post_status,
MAX(CASE WHEN pm.meta_key = '_sku' THEN pm.meta_value END) AS sku,
MAX(CASE WHEN pm.meta_key = '_regular_price' THEN pm.meta_value END) AS regular_price,
MAX(CASE WHEN pm.meta_key = '_sale_price' THEN pm.meta_value END) AS sale_price,
MAX(CASE WHEN pm.meta_key = '_stock' THEN pm.meta_value END) AS stock,
MAX(CASE WHEN pm.meta_key = '_weight' THEN pm.meta_value END) AS weight
FROM wp_posts p
LEFT JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE p.post_type = 'product' AND p.post_status = 'publish'
GROUP BY p.ID;That pivot pattern — MAX(CASE WHEN meta_key…) — is the whole trick for reading postmeta. Use the API as the source of truth and SQL to find what it left out.
Mapping
| WooCommerce | Medusa | Notes |
|---|---|---|
| Simple product | Product with one variant | Medusa always has variants |
| Variable product | Product with variants | Child posts become variants |
| Attribute taxonomy | Product option | Global attributes map cleanly |
| Custom attribute | Product option | Per-product, may need dedup |
| Product category | Product category | Hierarchy preserved |
| Product tag | Product tag | Direct |
| Customer (WP user) | Customer | Passwords excluded |
| Order (post) | Order | Import as records |
| Coupon | Promotion | Rules differ; re-express |
| `_regular_price` / `_sale_price` | Price / sale price | Decimal → integer minor units |
Variable products need care. In Woo, a variable product has child posts of type product_variation, each with its own meta including the attribute values it fixes. In Medusa a product declares its options and each variant supplies a value for every option. Variations that leave an attribute unset — "any size" — do not translate; expand them into explicit variants.
Prices are decimal strings: "29.99". Medusa wants 2999. Round explicitly rather than relying on float arithmetic:
const toMinorUnits = (value: string) =>
Math.round(parseFloat(value || "0") * 100)Half a cent of drift across ten thousand SKUs is a reconciliation problem nobody wants.
Plugins
The audit that determines your scope. Sort every active plugin into four buckets:
| Bucket | Examples | Action |
|---|---|---|
| Replaced by Medusa core | Multi-currency, advanced variants, order editing | Delete |
| Replaced by an integration | Email, reviews, analytics | Integrate directly |
| Genuinely custom | Your industry-specific logic | Build as a [module](/blog/medusa-v2-modules-explained) |
| Compensating for WordPress | Caching, security, performance | Gone by construction |
That last bucket is usually the largest, and it is the honest argument for the migration: a meaningful share of a Woo stack exists to make WordPress behave like an application server.
Customers and orders
Customers are WordPress users with billing and shipping meta. Passwords use WordPress's own hashing and cannot be migrated — plan a reset flow, as in any migration.
Orders are posts of type shop_order with line items in wp_woocommerce_order_items and wp_woocommerce_order_itemmeta. Import them as records, never replayed through checkout, and keep the original order number in metadata so support can find them.
Redirects
WooCommerce URLs depend on permalink settings, so check the live site rather than assuming:
| WooCommerce | Medusa storefront |
|---|---|
| `/product/<slug>/` | `/products/<slug>` |
| `/product-category/<slug>/` | `/collections/<slug>` |
| `/shop/` | `/collections` or `/products` |
| `/?p=123` | 301 to the mapped product |
Note the trailing slashes: Woo uses them, most Next.js storefronts do not. Handle both, or half your redirects 404. Export ranking URLs from Search Console rather than trusting the sitemap. Storefront SEO.
What improves
Stores usually see three things after migrating:
- Speed. No plugin chain on every request; static product pages instead of PHP rendering.
- Predictability. Updates stop breaking checkout, because there is no plugin ecosystem to conflict.
- Clean data. Products in tables designed for products rather than in generic post meta.
What you give up is the WordPress plugin ecosystem and the ability to change things without a developer. That is a real trade — see migrating to Medusa for whether it is yours to make.
Data quality is the hidden cost
WooCommerce stores accumulate mess, and the migration is when you meet it. Budget a few days to audit before importing, because importing bad data means cleaning it twice.
What to check, with the query that finds it:
-- Products with no SKU
SELECT COUNT(*) FROM wp_posts p
LEFT JOIN wp_postmeta m ON m.post_id = p.ID AND m.meta_key = '_sku'
WHERE p.post_type = 'product' AND (m.meta_value IS NULL OR m.meta_value = '');
-- Duplicate SKUs
SELECT meta_value, COUNT(*) c FROM wp_postmeta
WHERE meta_key = '_sku' AND meta_value != ''
GROUP BY meta_value HAVING c > 1;
-- Orphaned variations
SELECT COUNT(*) FROM wp_posts v
LEFT JOIN wp_posts p ON p.ID = v.post_parent
WHERE v.post_type = 'product_variation' AND p.ID IS NULL;Duplicate SKUs are the one that hurts: Medusa treats SKU as an identifier and your 3PL and ERP almost certainly do too. Resolve them before importing, not after.
Redirect edge cases specific to Woo
Beyond the main URL patterns, three things routinely get missed:
Attribute archive pages. /pa_colour/blue/ style URLs exist if attributes were made public, and they often carry links. Map them to your equivalent facet page or to the parent category.
Feed URLs. /feed/, /product/x/feed/ and comment feeds are indexed on most Woo stores. Redirect the useful ones and let the rest 410.
Uploads paths. /wp-content/uploads/… image URLs appear in Google Images and in other sites' hotlinks. Either keep serving them from the old host for a period or redirect them to the new asset URLs — the traffic is small but the effort is minutes.
We have pulled several stores out of post_meta. Ask about yours.
Frequently asked questions
How do I export data from WooCommerce for migration?
Use the WooCommerce REST API for correctly assembled objects and direct SQL against `wp_posts` and `wp_postmeta` for anything the API omits. The `MAX(CASE WHEN meta_key…)` pivot is the standard way to read post meta into columns.
How do WooCommerce variable products map to Medusa?
A variable product becomes a Medusa product whose options come from the Woo attributes, with each `product_variation` child post becoming a variant. Variations that leave an attribute unset must be expanded into explicit variants, since Medusa requires a value per option.
Can I migrate WooCommerce customer passwords?
No. WordPress uses its own hashing scheme and the hashes are not portable. Plan a password reset communicated as a security upgrade, with a login page that recognises migrated accounts.
What happens to my WooCommerce plugins?
They do not migrate. Audit them first: many are replaced by Medusa core functionality, several become direct integrations, a few need building as custom modules, and a large share exist only to compensate for WordPress and simply disappear.
How do I handle WooCommerce URL redirects?
Map `/product/<slug>/` to `/products/<slug>` and `/product-category/<slug>/` to your collection URLs, and handle the trailing slash difference explicitly. Pull the actual ranking URLs from Search Console rather than relying on the sitemap.
Is Medusa faster than WooCommerce?
Generally yes, substantially. A Medusa storefront serves statically generated pages from a CDN with a Node API behind it, rather than rendering PHP through a plugin chain on each request. The difference is most visible on category pages and under load.
Shopify to Medusa Migration: The Complete Technical Guide
Catalog, customers, orders, subscriptions and the redirect map that decides whether you keep your rankings. A working plan for moving off Shopify, from people who have done it.
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.
Magento to Medusa Migration: Leaving EAV Behind
Magento's EAV model, configurable products and multi-store hierarchy do not map one-to-one onto anything. How to extract, translate and cut over without losing B2B logic.



