Magento to Shopify Migration: What Ports, What Breaks

A Magento to Shopify migration moves the catalogue, customers and order history without much trouble. Four things need rebuilding instead: configurable products with more than three variant attributes, customer-group pricing, multi-store-view setups, and anything a PHP module did at checkout. Scope those first, then turn Magento’s url_rewrite table into 301s so no old URL 404s.

Should you migrate from Magento to Shopify at all?

Start with the support dates. Adobe gives each 2.4.x release three years of standard support from general availability, then a year of extended support on recent releases. According to the Adobe Commerce lifecycle policy, standard support for 2.4.6 ended on 11 August 2026, with extended support running to 31 August 2027, and 2.4.5 dropped out of extended support the same month. A store still on 2.4.5 or earlier is already unpatched, as of October 2026.

That is a reason to move off an old version, not necessarily off Magento. An upgrade to 2.4.8 buys standard support to May 2028.

Here is the test I would run. List every custom module and third-party extension on the store, and next to each one write where its job would live on Shopify: core admin, an app, a Shopify Function, a theme section, or nowhere. If most of the list lands in the first four, migrate. If “nowhere” holds the thing that makes the business money (a quoting engine, a bespoke pricing matrix, an ERP sync that rewrites orders mid-checkout), price the rebuild before anyone signs anything. Shopify will not run your PHP.

How do Magento configurable products map to Shopify variants?

Mostly well, with one hard wall.

A Magento configurable product is a parent plus a set of simple child products, and each child is a separate simple product with its own SKU, price and stock. Shopify’s model is one product with variants, which is the same shape: the parent becomes the product, each child becomes a variant, and the configurable attributes become options.

The wall is the option count. Shopify’s variant documentation is blunt: “Each product can have up to three options,” and “You can create up to 2,048 variants for a product.” The variant ceiling used to be 100; the developer changelog records it rising to 2,048 for all merchants on 15 October 2025. Nobody raised the three-option limit.

So audit the configurable attributes before you write an importer. A product configured on size, colour and length ports directly. One configured on size, colour, length and fit does not, and you have three choices: fold two attributes into one option (“Regular / 32”), split the product into several, or keep the fourth choice outside the variant model as a line item property the theme collects. Pick per product family, not per product, or the storefront ends up inconsistent.

Everything else about a Magento product goes somewhere ordinary. Non-variant attributes in the attribute set become metafields. Magento’s custom options (engraving text, a gift message) become line item properties. Bundle products have no native equivalent; the nearest is a cart transform Function, which means building or installing an app.

What happens to customers, groups and passwords?

Customers and addresses import fine. Passwords do not. Shopify’s customer import docs say you can’t migrate customer passwords from another store, so every migrated customer has to set a new password through an account invite. Plan the email for that before cutover, not after the first support ticket. Our post on moving customers without losing marketing consent covers the import itself, including the consent fields that are easy to drop on the way.

Customer groups are the harder part. Many Magento B2B stores run on groups plus tier prices: a trade group sees different prices and can order in different quantities. Shopify’s nearest model is B2B, built on companies, company locations and catalogs assigned to those locations. It is a different shape. A group is a flat label on a customer; a Shopify company has locations, and pricing attaches to the location. Shopify’s docs also say “your plan must support B2B capabilities,” so confirm the plan before you design around it. If the trade pricing is simple, a customer tag plus a discount may be all you need, and much less work.

Which Magento features have no direct Shopify equivalent?

Store views are the big one. A Magento install often has one website with several store views, one per language or country, each with its own URLs and sometimes its own catalogue. On Shopify that becomes one store with Markets. Shopify’s international domains guide recommends subfolders such as example.com/fr-ca for a first setup, which keeps everything on one domain. A store view that runs as a separate brand with its own catalogue may be better as its own Shopify store.

CMS blocks and widgets become theme sections and blocks, or metaobjects when the content is structured. Checkout customisation is the one to scope carefully: on Shopify you change checkout through Checkout UI extensions and Functions, inside a sandbox with fixed extension points. Anything a Magento module did to checkout that does not fit one of those points is gone.

How do you keep Magento URLs ranking after the move?

Magento’s own database already holds the redirect list. The url_rewrite table stores every storefront path with its entity_type, entity_id, request_path, redirect_type and store_id. Rows with redirect_type 0 are live URLs; rows with 301 are old URLs Magento already redirects, kept because save_rewrites_history defaults to on. You want both, because Google still has some of the old ones.

Magento’s defaults make every product and category URL end in .html. That is set in the Catalog module’s config.xml as product_url_suffix and category_url_suffix. Shopify serves /products/<handle> and /collections/<handle> with no suffix, so every single catalogue URL changes.

Export the rewrites for one store view, resolving configurable children to their parent so a child’s page redirects to the product that now holds it as a variant:

mysql -B magento -e "
SELECT r.entity_type, r.entity_id,
       COALESCE(l.parent_id, r.entity_id) AS owner_id,
       r.request_path, r.redirect_type
FROM url_rewrite r
LEFT JOIN (
  SELECT product_id, MIN(parent_id) AS parent_id
  FROM catalog_product_super_link
  GROUP BY product_id
) l ON r.entity_type = 'product' AND l.product_id = r.entity_id
WHERE r.store_id = 1
  AND r.entity_type IN ('product', 'category')" > rewrites.tsv

Then turn that into Shopify’s redirect CSV. This assumes your product importer set each Shopify handle to the Magento url_key, and each collection handle to the category path with slashes replaced by hyphens. If your importer did something else, change toHandle to match it.

// magento-redirects.mjs
// Usage: node magento-redirects.mjs rewrites.tsv > redirects.csv
import { readFileSync } from 'node:fs';

const SUFFIX = /\.html$/; // match catalog/seo/*_url_suffix on your store
const [header, ...lines] = readFileSync(process.argv[2], 'utf8').trim().split('\n');
const cols = header.split('\t');
const rows = lines.map((line) => {
  const values = line.split('\t');
  return Object.fromEntries(cols.map((col, i) => [col, values[i]]));
});

// Live (redirect_type 0) path per entity. The shortest has no category prefix.
const canonical = new Map();
for (const r of rows) {
  if (r.redirect_type !== '0') continue;
  const key = `${r.entity_type}:${r.entity_id}`;
  const current = canonical.get(key);
  if (!current || r.request_path.length < current.length) {
    canonical.set(key, r.request_path);
  }
}

const toHandle = {
  product: (path) => path.split('/').pop().replace(SUFFIX, ''),
  category: (path) => path.replace(SUFFIX, '').replaceAll('/', '-'),
};
const prefix = { product: '/products/', category: '/collections/' };

const seen = new Set();
const out = ['Redirect from,Redirect to'];
for (const r of rows) {
  const ownerPath = canonical.get(`${r.entity_type}:${r.owner_id}`);
  if (!ownerPath) {
    console.error(`No live URL for ${r.entity_type} ${r.owner_id}: ${r.request_path}`);
    continue;
  }
  const from = `/${r.request_path}`;
  if (seen.has(from)) continue;
  seen.add(from);
  out.push(`"${from}","${prefix[r.entity_type]}${toHandle[r.entity_type](ownerPath)}"`);
}
console.log(out.join('\n'));

Three limits from Shopify’s URL redirect docs shape what happens next. Redirects only fire for paths that 404 on Shopify, which every .html path will. A standard plan holds 100,000 redirects and Plus holds 20,000,000, so a large catalogue with years of rewrite history can overflow a standard plan; drop the oldest 301 rows first. And query-string URLs “might not work as expected”, which matters for Magento’s layered navigation (?color=12). Let those go, or handle them at the edge. For a catalogue big enough that the admin CSV import gets painful, our WooCommerce redirect walkthrough shows the bulk import through the Admin API, and the reserved paths Shopify refuses to redirect apply here too.

Run the export once per store view. If a French store view becomes the /fr-fr subfolder, prefix the targets to match.

What I would actually do

I would migrate a Magento store whose custom code is mostly catalogue, content and integrations, and whose checkout is close to stock. I would audit configurable attributes and customer-group pricing in the first week, because those two decide whether the catalogue ports or gets remodelled. I would build the redirect CSV from url_rewrite before the importer is finished, so the handle rules are agreed once and both sides use them.

I would not migrate a store whose margin lives in a custom checkout module, or one that would need a separate Shopify store per store view, until someone has costed the rebuild against simply upgrading to 2.4.8.

We run migrations onto Shopify, from the catalogue audit through to the redirect map. If you are weighing the move, our Shopify development page explains how we scope that work.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch