The hard part of a Magento to Shopify Plus migration is not the export. It's deciding, attribute by attribute, where 400 EAV attributes are allowed to land in a platform that gives you three product options, one flat variant table, and metafields for everything else. Get that mapping wrong and you discover it three weeks after launch, when filters return nothing and 6,000 configurable products have collapsed into orphaned simples. So before anyone touches a CSV, build a mapping sheet: one row per Magento entity or attribute, one column for its Shopify destination, one column for the transform rule, one column for who signs off.
What follows is the checklist we work through on every Magento 2 project, in roughly the order it matters.
The EAV-to-flat problem, stated plainly
In Magento 2 a single product is scattered across catalog_product_entity plus the varchar, int, decimal, text and datetime value tables, joined by attribute_id and scoped by store_id. Attributes belong to attribute sets. Values can differ per store view. Nothing enforces that any of it is consistent.
Shopify has a product, up to three options, variants, and metafields. That's the whole vocabulary. Every decision in the migration is a compression: which Magento attributes earn a spot as an option, which become metafields, which become tags, and which get dropped because nobody has queried them since 2019.
Two hard ceilings to design against. Three options per product, still. Variant counts have gone up substantially on recent API versions, so a 400-variant product is no longer an automatic blocker, but a Magento configurable built on colour, size, fit and material is. You either merge two axes into one concatenated option ("Slim / 32"), split into sibling products linked by a metafield, or drop the least-used axis. Pick one rule and apply it to the whole catalog, because mixed approaches make the collection filters unusable.
Product types: what maps cleanly and what doesn't
- Simple → product with a single variant. Clean.
- Configurable → product with options and variants. Clean if you're within three axes. The child simples must be discarded as standalone products, not left published, or you'll index duplicates.
- Virtual → product with "this is a physical product" unchecked. Clean.
- Downloadable → product plus a digital delivery app. The file, link expiry and download-limit fields have no native home.
- Grouped → no equivalent. Either a Shopify bundle, or separate products stitched together on the PDP by a metaobject reference. Decide per SKU, not globally.
- Bundle → Shopify Bundles handles fixed sets. Dynamic bundles with per-option price rules need the Cart Transform API and a developer, and that's a build, not a mapping row.
- Custom options (engraving, gift note, cut-to-size) → line item properties, driven by a theme block or an app. Priced custom options are the awkward ones: Shopify has no native price delta on a line item property, so you either create variants or add a hidden fee product.
Also on the list: tier prices, special_price with from/to dates, and catalog price rules. Tier pricing maps to B2B catalogs and price lists on Plus, with quantity breaks. Special prices become compare-at-price plus a scheduled discount. Cart price rules generally become automatic discounts or Shopify Functions, and about a third of them turn out to be dead rules nobody remembers writing.
The three buckets every Magento attribute falls into
Export your attribute list with usage counts. Then sort every row into one of three destinations.
Bucket 1: native Shopify fields. name, sku, price, special_price, weight, barcode/EAN, cost, status, visibility, short_description, description, meta_title, meta_description, url_key. Map these first because they're free.
Bucket 2: metafields. Everything a customer reads or a filter uses. Fabric composition, wattage, warranty period, ingredient list, care instructions, country of origin, HSN code, shelf life, FSSAI licence number. Give each one a proper definition with a type — not all single-line text. Dimensions become a dimension type so you can render units. Ingredient lists become a list of single-line text so they render as bullets instead of a comma soup. Anything you want as a storefront filter must be a defined metafield with the right type; ad-hoc metafields written by an import script won't appear in Search & Discovery.
Bucket 3: tags, product type, or the bin. Merchandising flags (new_arrival, bestseller, gift_under_1000) are tags and automated collection conditions. Attribute sets usually map to product type or the Shopify product taxonomy category, not to metafields. And then there's the bin: attributes with fewer than 50 non-null values, legacy ERP mirror fields, and every "custom_field_3". Be aggressive here. A catalog that arrives with 60 clean, defined metafields is far more useful than one carrying 380 attributes nobody trusts.
Media, swatches and the image role problem
Magento assigns roles to gallery images: base, small_image, thumbnail, swatch_image. Shopify has an ordered media list plus one image assignment per variant. So role data becomes position data, and you need an explicit rule: role=base becomes position 1, everything else follows gallery position, thumbnails are discarded because Shopify generates its own sizes.
Visual swatches are the common trap. In Magento a colour swatch is an attribute option with its own image, independent of product photography. Shopify has no swatch entity, so either the theme derives swatches from variant images (fine for apparel, wrong for paint or laminates) or you build a metaobject per colour holding the swatch asset and reference it from the option value. Do the metaobject version if the brand has more than a dozen colourways; retrofitting it later means touching every product.
One practical note: pull media from the Magento filesystem, not by scraping the storefront. You'll get the originals rather than resized cache renditions, and Shopify's CDN will do a better job with them.
Categories, URL rewrites and the redirect map
Magento's category tree is nested and doubles as navigation. Shopify collections are flat. So a three-level tree becomes: top-level collections, plus tag-driven or metafield-driven automated collections for the leaves, plus a menu structure that fakes the hierarchy. Anchor categories with layered navigation become collection filters. Category-level content (description blocks, banner images) becomes collection description plus metafields.
Then the redirects, which is where organic traffic is won or lost. Magento's url_rewrite table holds every historical rewrite, including the ones generated when someone renamed a category in 2021. Export it, deduplicate, and map:
/category/subcategory/product.html→/products/handle/category/subcategory.html→/collections/handle- Layered nav URLs with
?color=12&size=8→ the closest collection, filters stripped - CMS pages →
/pages/handle - Blog posts →
/blogs/news/handle
Import as 301s in batches via the redirect CSV or the GraphQL import. Two things people forget: pull your top 500 landing pages from Search Console and GA4 and verify each one by hand, and keep a catch-all plan for discontinued SKUs. Redirecting a dead product to the homepage is worse than a 404; send it to its parent collection. If organic is a meaningful share of revenue, this is worth a proper content and SEO pass rather than a spreadsheet exercise.
Customers, groups and the password you cannot bring
customer_entity and customer_address_entity map straightforwardly: email, first/last name, phone, accepts_marketing, default billing and shipping, plus additional addresses. Two gotchas.
Password hashes don't migrate. Magento uses its own hashing, Shopify won't accept it. This used to mean a mass password-reset campaign and a support queue. With Shopify's new customer accounts, login is a one-time code sent to email, so there's no password to reset and no migration step at all. If you're on classic accounts for some reason, budget for the reset emails and the drop-off.
Customer groups are the other one. Wholesale and Retailer groups map to B2B companies and catalogs on Plus. Tax-exempt groups map to tax exemptions. Everything else — VIP, Employee, Repeat — becomes customer tags feeding segments. Tag naming matters more than it looks, because segments and Flow triggers will be built on those strings for years.
Orders, credit memos and how much history to carry
You do not need eight years of orders in Shopify. Carry 24 months, plus every customer record regardless of order age, plus a full historical export sitting in your warehouse or in Google Cloud Storage for finance and GST purposes. Migrating a decade of sales_order rows slows the project and adds nothing your reports will use.
Map sales_order → order, sales_order_item → line items (matched on SKU, which is why SKU hygiene comes first), sales_order_address → billing/shipping, sales_shipment → fulfillment, sales_invoice → transaction, sales_creditmemo → refund. Set processed_at to the original order date so returning-customer logic, cohort reports and LTV segments behave. Payment gateways come across as manual transactions with a gateway label: name them properly — Razorpay, PayU, COD — because Indian brands do want to segment COD versus prepaid buyers later, and an RTO analysis on "manual" tells you nothing. Partial credit memos rarely round-trip perfectly; accept small variances and note them for your accountant rather than trying to force parity.
GST, HSN and the fields Shopify doesn't have
Shopify's built-in tax engine has no concept of HSN codes, CGST/SGST/IGST splits or place-of-supply rules. So the mapping is: HSN code becomes a product metafield, GST rate slab becomes either a metafield or a tax override per collection, and invoicing goes to an app that reads both. Capture GSTIN at checkout with a checkout UI extension and store it on the order and the customer. FSSAI licence numbers for food brands go into a metafield rendered on the PDP and on the invoice template.
Also on the India list: qty_increments and min_sale_qty have no native equivalent, so case-pack rules need a cart validation function. Multi-source inventory maps to Shopify locations, one per Magento source, with the priority order rebuilt as fulfillment priority. Pin codes and serviceability logic sit with your shipping aggregator, not with Shopify.
Is Shopify Plus the right target at all?
Sometimes the honest answer is no. If the reason for leaving Magento is hosting bills, extension conflicts and a developer you can't reach on Diwali weekend, Shopify Advanced may carry you fine, and the mapping work above is identical. Plus earns its price when you need B2B companies with catalogs and price lists, several Markets or expansion stores, higher API limits, custom checkout logic, or Launchpad for festive drops. A single-market D2C brand with 900 SKUs and no wholesale arm does not need it on day one, and Plus is a one-click upgrade later. We'd rather tell you that than sell you a plan you'll grow into in three years. If you do want the Plus route mapped properly, that's what our migration practice does day to day.
Dry runs, the SKU freeze and the sign-off
Run the migration twice before you run it for real. The first pass exposes the data you didn't know was broken: duplicate SKUs, blank SKUs, configurables with no children, products assigned to deleted categories, images referenced in the DB but missing from pub/media. Fix at source in Magento where you can; the alternative is transform logic that nobody can maintain.
Then freeze. No new products, no attribute edits, no category restructuring from the final export until go-live. Orders and inventory keep flowing and get delta-synced. Every merchandiser will ask for an exception. Say no.
Sign-off checklist we use before DNS: 20 sampled products verified field by field against Magento, including metafields and variant images; every automated collection returning a sane count; top 500 URLs returning 301 to a live page; a test order through Razorpay and COD each, with GST invoice generated; customer login working via emailed code; three historical orders visible in the correct customer's account with the right dates.
If you're at the point of scoping this and want a second opinion on the attribute mapping before it's locked, send us the Magento attribute export and a category tree dump and we'll mark up where the compression will hurt. A free audit is the least painful place to find that out.


