Migrate a Custom Ecommerce Site to Shopify: Data, URLs and Cutover
How to migrate a custom ecommerce site to Shopify: map your data, import products, customers and orders, keep SEO with 301 redirects, and cut over safely.
Long Nguyen
Fullstack Developer · AI Engineer · Researcher
When it makes sense to migrate a custom ecommerce site to Shopify
Migrating a custom ecommerce site to Shopify makes sense when most of your code is commodity commerce plumbing: cart, checkout, payments, tax, order admin, security patches. Shopify runs all of that as a managed service, so the engineering hours you free up go back into the parts of the business that actually differentiate you.
It is the wrong move when the custom behavior is the business. Contract pricing engines, product configurators and quote workflows can be rebuilt on Shopify, but you are rebuilding them, not migrating them.
| Situation | Verdict | Why |
|---|---|---|
| Most dev time goes to checkout, payments, PCI scope and uptime | Migrate | Shopify takes over those layers. |
| Standard catalog, standard fulfilment, a few integrations | Migrate | Nothing here needs custom code to survive the move. |
| Pricing, quoting or configuration logic is your competitive edge | Stay custom, or go headless | Keep your logic and use Shopify as the commerce backend through the Storefront API. |
| Products need more than three option types that all change price or SKU | Redesign the catalog first | Shopify caps products at three options; the rest must become metafields, separate products or an app. |
| You depend on direct SQL access to live order data | Hybrid | Shopify exposes data through APIs and exports, not your own database. |
The middle path is worth knowing about: a headless build keeps your own frontend and talks to Shopify for catalog, cart and checkout. It costs more than a theme-based store, so choose it for a reason, not by default.
What data moves to Shopify and what does not
Before touching a CSV, know which data has a native route into Shopify and which needs an API script or an app. This table is the scoping tool: every row that says "no native import" is engineering time on your quote.
| Data | How it gets in | The catch |
|---|---|---|
| Products and variants | Product CSV import (UTF-8, 15 MB per file) or the Admin API | 3 options per product; up to 2,048 variants per product since the October 2025 increase from 100. |
| Product images | Image URLs in the CSV | URLs must be publicly reachable. Sorting the CSV in a spreadsheet can detach images from their products. |
| Customers | Customer CSV (15 MB per file) | Passwords cannot be migrated, and the CSV does not import order history or lifetime totals. |
| Order history | Admin API or a migration app | No native order CSV import in the admin. |
| URL redirects | Redirect CSV import | 100,000 per store (20,000,000 on Plus), and only from URLs that are currently broken. |
| Custom product fields | Metafields and metaobjects | Create the definitions before importing values. |
| Reviews, wishlists, loyalty points | The importer of whichever app replaces them | Pick the replacement app first, then export in the format it accepts. |
Map your custom database to Shopify's data model first
Most migrations that go wrong fail at the mapping stage, not at the import button. Write a field-by-field map from your tables to Shopify objects before exporting a single row.
| Your schema | Shopify target | Rule of thumb |
|---|---|---|
| products + product_variants | Product + Variants | One unique Handle per product. Reuse your old slug so the redirect map stays one-to-one. |
| Attributes (size, color, material, finish...) | Options or metafields | If the attribute changes price or SKU, it takes an option slot (max 3). If not, make it a metafield. |
| Category tree | Collections plus navigation menus | Collections are flat. Rebuild the hierarchy in menus, and use smart collections for rule-based groups. |
| Stock per warehouse | Inventory locations | One Shopify location per physical stock location. |
| Customer groups, tiered or contract pricing | A design decision | Not a column mapping. See the custom-features section below. |
Here is a short export script that turns one-row-per-variant data into Shopify's product CSV. Only the first row of each product carries the title, description and image; the remaining rows add variants. Split the output into files under 15 MB.
import csv
from collections import defaultdict
rows = load_variants() # your own DB export, one dict per variant
by_product = defaultdict(list)
for r in rows:
by_product[r["product_slug"]].append(r)
cols = ["Handle", "Title", "Body (HTML)", "Vendor", "Published",
"Option1 Name", "Option1 Value", "Variant SKU", "Variant Price",
"Variant Inventory Tracker", "Variant Inventory Qty", "Image Src"]
with open("products_001.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=cols)
w.writeheader()
for slug, variants in by_product.items():
for i, v in enumerate(variants):
first = i == 0
w.writerow({
"Handle": slug,
"Title": v["title"] if first else "",
"Body (HTML)": v["html"] if first else "",
"Vendor": v["vendor"] if first else "",
"Published": "true" if first else "",
"Option1 Name": "Size",
"Option1 Value": v["size"],
"Variant SKU": v["sku"],
"Variant Price": f"{v['price']:.2f}",
"Variant Inventory Tracker": "shopify",
"Variant Inventory Qty": v["qty"],
"Image Src": v["image_url"] if first else "",
})
Run a test import with five to ten products first and open them in the admin. Price format (a dot, no thousands separator), the inventory tracker value and option names are where most first imports break.
Large catalogs have one more constraint to plan around. Once a store holds more than 50,000 variants, Shopify throttles the creation of new variants to 1,000 per day unless the store is on Plus. Check the current rule for your plan before you promise a launch date for a 100,000-SKU catalog.
Import products, then customers, then orders
The order matters because each step references the previous one. Orders point to products and customers; customers are matched by email. Importing out of order means re-linking data by hand later.
- Products first. Orders reference SKUs and handles, so the catalog must exist before anything else.
- Customers second. Deduplicate on email and phone before exporting. Shopify skips duplicate emails and phone numbers during import and keeps only the last profile for a duplicated email, so silent data loss is easy. Carry over marketing consent honestly: set Accepts Email Marketing to yes only for customers who actually opted in.
- Orders last. There is no order CSV import in the Shopify admin, so history goes in through the Admin API or a migration app. Disable customer and staff notifications for the run so nobody gets an email about an order from two years ago.
Two details from Shopify's customer import documentation change your launch communications. First, passwords cannot be migrated, so every returning customer has to be invited to set a new one; plan that email. Second, the Total Spent and Total Orders columns are not imported, so lifetime value on a customer profile rebuilds only from the orders you import.
If your order history or catalog is too big or too irregular for CSV tooling, a scripted import over the Admin API with retry logic and a reconciliation report is the safer route. That is the kind of job covered by Netalith's eCommerce data migration service.
Keep your SEO: build the 301 redirect map before launch
Shopify fixes its URL structure: products live under /products/, collections under /collections/, pages under /pages/ and blog posts under /blogs/. Unless your custom site already used those patterns, almost every indexed URL changes, which makes the redirect map the real SEO migration.
Build the list from the sources that show which URLs matter: your sitemap, your database, Search Console performance data and backlink exports. Every URL with traffic or links needs a destination. Then check the constraints in Shopify's URL redirect documentation: the limit is 100,000 redirects per store (20,000,000 on Plus), and a redirect only fires from a URL that is currently broken. If an old path matches a live Shopify path, the redirect is ignored.
This script writes the import file and refuses the mistakes that cause silent failures:
import csv
mapping = [(p["old_path"], f"/products/{p['slug']}") for p in load_products()]
live_paths = {new for _, new in mapping}
seen = set()
with open("redirects.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["Redirect from", "Redirect to"])
for old, new in mapping:
assert old not in live_paths, f"old path is also a live path: {old}"
assert old not in seen, f"duplicate source: {old}"
seen.add(old)
w.writerow([old, new])
Redirects only work on requests that reach the Shopify store, so you cannot fully test them until the domain points at Shopify. Test the mapping on a staging domain with the same file, then re-verify on launch day by crawling your old URL list and confirming each one returns a 301 to a page that returns 200. Keep titles, headings and body copy unchanged where you can; changing URLs and content in the same release makes any ranking drop impossible to diagnose.
Rebuild custom features the Shopify way
Audit every custom feature and tag it one of four ways: native Shopify, an app, a custom app, or drop it. A surprising share of custom code is a workaround for something the old stack lacked and Shopify already ships.
| Custom feature on your old site | Shopify route |
|---|---|
| Custom discount or pricing rules | Shopify Functions (discount logic) or a discount app |
| Extra product data and spec tables | Metafields and metaobjects rendered in the theme |
| Extra checkout fields or validation | Checkout extensibility (UI extensions and Functions) |
| ERP, warehouse or shipping sync | A custom app on the Admin API with webhooks |
| Internal admin screens | An embedded custom app |
| Bespoke storefront behavior | Theme code in Liquid, or a headless storefront on the Storefront API |
Do this audit before you quote the project. The import is a few days of work; the custom features decide whether the migration takes weeks or months.
Plan the cutover so you can roll back
A safe cutover keeps the old site intact until the new one has proven itself.
- Build and QA on the Shopify store behind its storefront password, with test orders through the full checkout and fulfilment path.
- Announce a freeze window and stop catalog and customer edits on the old site.
- Run a delta import: products changed, customers created and orders placed since your first export.
- Lower the DNS TTL a few days ahead, so the switch and any rollback propagate quickly.
- Point the domain at Shopify, import the redirect file, and crawl your old URL list to confirm every 301 lands on a 200.
- Submit the new sitemap in Search Console and watch 404s, indexing and orders daily for the first two weeks.
Rollback is simply repointing DNS to the old site, which works only if that site stays running and unchanged. The real cost of a rollback is reconciliation: any order placed on Shopify in the meantime has to be copied back, so decide your go or no-go criteria before launch day rather than during it.
Get your migration scoped
If you want a second set of eyes on the data mapping, the redirect file or the custom-feature audit before you commit, send the details through Netalith's free quote form. Pricing is set to scope, so the more precisely you list data volumes and custom features, the more accurate the estimate.
FAQ
Frequently asked questions
How long does it take to migrate a custom ecommerce site to Shopify?
The catalog import itself takes hours. The schedule is driven by data mapping, rebuilding custom features and QA, so the number of custom features matters far more than the number of products. Audit the custom features first and the timeline follows from that list.
Will I lose my SEO rankings when I migrate to Shopify?
Some movement right after launch is normal. The avoidable losses come from missing 301 redirects, changed content and changed titles. Redirect every URL that has traffic or backlinks, keep on-page content the same where possible, and monitor Search Console for the first two weeks.
Can I keep my existing product and category URLs on Shopify?
Not for products, collections and pages. Shopify fixes those under /products/, /collections/ and /pages/. Reuse your old slugs as Shopify handles to keep the mapping one-to-one, then 301 redirect every old URL to its new one.
Can customers keep their passwords after the migration?
No. Shopify does not allow passwords to be migrated from another store by CSV, so imported customers must be invited to create a new password. Plan an email to returning customers around launch.
Can I import my old order history into Shopify?
Yes, but not through a native CSV import in the admin. Historical orders go in through the Admin API or a migration app, after products and customers are already in the store.
What are Shopify's variant and option limits?
A product can have up to 2,048 variants (raised from 100 in October 2025) but still only three options. Attributes beyond three that do not change price or SKU belong in metafields.
Can I keep my custom frontend and only move the backend to Shopify?
Yes. A headless setup keeps your own storefront and uses the Storefront API for catalog, cart and checkout. It costs more to build and maintain than a theme, so use it when your frontend logic is a real differentiator.