The client was on Shopify Advanced at 280 EUR/month, 12,000 SKUs, 4 warehouses, B2B + B2C dual pricing, 65% organic traffic from SK and CZ Google. The headless migration to Medusa.js finished after 4 months, two weeks before Black Friday. No organic ranking was lost, no in-flight order was lost, and the Stripe integration went from 14 manual steps per new product to zero. This article is what we did, what we botched, and what we'd do differently.
Why headless — and when NOT
Headless commerce has two valid reasons:
1. **Flexibility of custom checkout / B2B / multi-storefront.** Shopify (Advanced and Plus) has a fixed checkout pipeline. For B2B with net-60 terms, custom pricing tiers, multi-warehouse splitting or complex tax rules (SK-DE-AT cross-border), Shopify becomes a fight with API limits and Liquid templates. 2. **Storefront performance + UX.** A Next.js storefront with ISR (Incremental Static Regeneration) + edge caching reaches LCP < 1.2 s on 4G. A Shopify Liquid-rendered storefront has LCP 2.5–4.0 s. For an SEO-driven business that's a measurable revenue difference.
**When NOT headless:** - Catalogue < 500 SKUs + standard B2C checkout → Shopify or BigCommerce stays cheaper and faster - The team has no capacity for a custom Next.js storefront (custom CMS, custom checkout, integrations) - The payments / accounting / marketing roadmap is all Shopify-native (Shopify Payments, Shopify Tax, Shopify Email)
For those clients the smoother existing "semi-headless" — Shopify Hydrogen + Oxygen — gives a headless storefront frontend without leaving the Shopify ecosystem.
Stack decisions in 2026
Backend: Medusa.js vs Saleor vs Commerce.js
**Medusa.js v2.x** - Node.js, PostgreSQL, Redis - Modular, plugin architecture - Best developer experience in 2026 - Weaker B2B feature set out-of-the-box (plugin or custom code needed for quotes, approvals, customer hierarchy) - **Price:** open-source self-hosted, or Medusa Cloud from 200 EUR/month - **When:** B2C + light B2B, dev team with Node experience
**Saleor** - Python, Django, GraphQL-first - Strongest B2B feature set (customer groups, channel-based pricing, quote requests) - Multilanguage + multi-currency native - Steeper learning curve, GraphQL overhead - **Price:** open-source self-hosted, or Saleor Cloud from 500 EUR/month - **When:** B2B-first, multi-storefront, complex pricing
**Commerce.js / Swell.is** - Hosted-only, no self-host - Fast setup, but vendor lock-in - **When:** PoC / MVP, small catalogue
For a 12k-SKU SK retailer with a 30% B2B share, we chose **Medusa.js v2** — self-hosted on Hetzner Cloud (CCX23, 4 vCPU + 16 GB RAM, 35 EUR/month), PostgreSQL 16 on a separate node, Redis for cache + queue.
Frontend: Next.js 16 with App Router
- ISR (Incremental Static Regeneration) for product pages — revalidate every 60s
- Edge runtime for /api routes (Cloudflare Workers or Vercel Edge)
- Tailwind 4 + shadcn/ui for fast UI development
- Server Components for product detail (fast TTFB), Client Components for cart / interactive elements
- Bundle size target: < 180 KB shipped JS for home page
Peripheral services
| Function | 2026 choice | Price | |----------|-------------|-------| | Search | Algolia | 0.50 EUR / 1k search ops, ~180 EUR/month for 12k SKUs + 400k monthly searches | | Payments | Stripe (Standard + Connect for B2B) | 1.4% + 0.25 EUR per EU transaction, B2B Connect 0.5% flat | | Email / Marketing | Klaviyo | 90 EUR/month for 10k profiles | | Observability | Sentry + PostHog | Sentry 26 EUR/month, PostHog 0 EUR (free tier up to 1M events) | | Image CDN | Cloudflare Images | 5 EUR/month + 1 EUR / 100k delivery | | DAM (Digital Asset Management) | Cloudinary or self-hosted Imgproxy | 90 EUR/month or 15 EUR self-hosted | | Tax | Avalara AvaTax (SK + EU OSS) | 250 EUR/month | | Shipping | Packeta API + Slovenská pošta API | 0 EUR (consumption-based) | | Reviews | Trustpilot Business | 200 EUR/month |
Total OPEX: ~750 EUR/month (vs. Shopify Advanced 280 EUR + superfluous Shopify Apps 350 EUR = 630 EUR originally).
Migration plan — 4 months, 6 phases
Month 1: Audit + data preparation
**Weeks 1–2: Catalog audit** - Export Shopify product CSV + metafields export - Mapping Shopify product schema → Medusa product schema (variants, options, prices, tax_rates) - Identification of data quality issues (missing SEO descriptions, duplicate handles, broken images) - 12,000 SKUs decomposed into: 9,200 unique products × on average 1.3 variants = 12,100 product_variants in Medusa
**Week 3: URL audit for 301 mapping** - Crawl of all Shopify URLs via Screaming Frog → 14,800 unique URLs - Categorise: product pages (12,000), collection pages (240), CMS pages (45), pagination URLs (2,200), other (300) - Plan: preserve product handle (URL slug) → same handle in Medusa → 0 redirects needed for 11,800 URLs - Plan: collection pages mapping → mostly 1:1, but Shopify smart collections don't map directly to Medusa product categories → 80 manual mappings - Pagination URLs (?page=2) → same structure in Medusa, naturally preserved
**Week 4: Customer + order export** - 18,400 customers with activity in the last 24 months → import into Medusa - 47,000 historical orders → migrate to a read-only archive in Medusa (not needed in the active system but needed for legal compliance + customer support) - B2B customer groups + pricing tiers (Bronze/Silver/Gold) → Medusa customer_group + price_list
Month 2: Backend build + integrations
**Weeks 5–6: Medusa setup + custom modules** - Hetzner Cloud setup, Coolify for Docker orchestration, GitHub Actions CI/CD - Medusa modules: product, order, customer, fulfillment, payment (Stripe), tax (Avalara plugin) - Custom modules: - SK-specific tax handling (VAT 23%, reduced 5%, exempt) - SK-specific invoicing (requires IČO, DIČ, IČ DPH, issue date, delivery date, due date) - B2B quote request flow (custom Medusa workflow + admin UI)
**Weeks 7–8: Peripheral integrations** - Algolia: products + variants index, faceting (category, price, brand, availability), synonyms (SK + CZ) - Klaviyo: customer + order webhook → Klaviyo events (cart_abandoned, ordered_product, etc.) - Avalara: order_placed webhook → AvaTax calculation → store tax breakdown on the order - Sentry: backend + frontend error tracking, source maps upload
Month 3: Frontend build + SEO continuity
**Weeks 9–10: Storefront core pages** - Next.js 16 setup, Tailwind 4, shadcn/ui - Home, Category, Product Detail, Cart, Checkout (multi-step), Account, Order History - ISR for Product + Category (revalidate 60s) - Server Components default, Client Components only for cart/wishlist state
**Week 11: SEO migration** - **Sitemap continuity:** Next.js sitemap.xml generates the same URLs as Shopify, plus new URLs if any - **301 redirect map:** /apps/customer-portal/* → /account/*, /collections/ → /c/* (we changed the category URL structure) - **Structured data:** Product schema (price, availability, sku, brand, aggregateRating, review), Offer schema, BreadcrumbList, Organization - **Open Graph + Twitter Card:** og:image generated via a Next.js dynamic OG image route (1200×630, branding, product hero, price) - **robots.txt + meta robots:** preserve noindex for /account/*, /cart, /checkout - **Canonical URLs:** explicit canonical on every product page, reason: Shopify used it with the ?variant= parameter, Medusa does it via a separate URL or client-side state, you need canonical on the base product URL
**Week 12: Cutover plan** - DNS cutover plan (TTL 60s 24h before cutover) - Database snapshot Shopify → Medusa import (last delta) - Stripe webhook switch (Shopify Stripe → Medusa Stripe) - Klaviyo webhook switch - Customer notification: "the shop will be unavailable Saturday night 02:00–04:00"
Month 4: Cutover + stabilisation
**Week 13: Soft launch** - Subdomain shop-new.domain.sk → Next.js storefront + Medusa backend - 5% of traffic routed via Cloudflare Workers split test - Sentry watch: zero new errors over 48h - Klaviyo monitoring: cart abandonment rate stable
**Week 14: Full cutover** - DNS cutover (apex domain → new infra) - 301 redirect map deployed on Cloudflare Workers (already running pre-DNS cutover) - Shopify stays in read-only mode for 30 days (legal archive of past orders) - Customer support ticket spike monitor
**Week 15: Post-launch fix** - 12 hours of bug fixing in the first 72h (none critical, but 8 minor — checkout edge cases, B2B quote workflow, several Algolia synonym issues)
**Week 16: Performance tuning + SEO check** - Lighthouse audit, LCP target < 1.5s (achieved 1.1s) - Search Console: indexing status check, no new 404 errors, no ranking drop over 5% on the top 50 keywords - A/B test new product detail page vs Shopify-style → +14% conversion rate
What hurt (and what we'd do differently)
Slow: SK-specific invoicing
**Plan:** 4 days of development for a custom invoicing module.
**Reality:** 3 weeks.
The Slovak legal requirement for an invoice lists 14 mandatory fields (§ 74 of the VAT Act), plus electronic invoicing for B2B over 5,000 EUR has its own EDI requirements (E-invoice Financial Administration). Plus VAT OSS for cross-border EU sales needs per-transaction reporting into MyTaxOffice. Plus invoice-line formats for cash transactions have their own rules.
**Lesson:** size SK/EU compliance overhead at 3× the original estimate. No EU compliance feature is "2-day implementation."
Fast: Algolia sync
**Plan:** 5 days for Algolia integration + indexing.
**Reality:** 2 days.
Medusa has a first-party Algolia plugin (`@medusajs/algolia`) that emits webhook events (product.created, product.updated, product.deleted) → Algolia reindex. Setup was API key configuration + index schema. Real time 2 days.
**Lesson:** look for first-party / community plugins first. If one exists with 1k+ GitHub stars, don't start a custom integration.
Slow: B2B quote workflow
**Plan:** 1 week.
**Reality:** 3 weeks.
A B2B customer wants a "quote request" for orders over 5,000 EUR. Workflow: 1. Customer adds products to cart → clicks "Request a quote" (instead of "Add to cart") 2. Quote appears in the admin panel, the sales rep approves it and optionally adjusts price / shipping / payment terms 3. Customer receives an email with a link to the quote → can accept (creating an order with the adjusted conditions) or reject
Medusa v2 has a `quote` module in alpha in 2026, but the workflow + UI we had to build custom. Plus admin permissions for various sales reps. Plus SK email templates.
**Lesson:** B2B-specific workflows in headless commerce are always custom. Saleor might have saved 1 week here (it has a built-in quote module in stable), but the Medusa decision was justified by developer experience for the rest of the stack.
Fast: 301 redirect SEO retention
**Plan:** 1 week for 301 mapping + monitoring.
**Reality:** 3 days.
The cipher: preserve the product handle (URL slug) from Shopify into Medusa. The Shopify URL was `/products/product-handle`, Medusa uses the same pattern. 11,800 of 14,800 URLs stayed identical — no redirect needed.
For the rest (collections, customer portal, a few CMS pages) we deployed a 301 map on Cloudflare Workers (`URL Rewrite Rules`) — 280 explicit rules + 12 regex patterns. **Zero indexed URLs lost** in Search Console after 60 days.
**Lesson:** if you can keep the URL pattern, do. If you must change, build the redirect map *before* cutover, not after.
SEO checklist — what to verify before cutover
1. **Sitemap parity:** Next.js sitemap.xml must contain all URLs from the original Shopify sitemap. Diff via a `sitemap-diff` script in CI. 2. **301 redirect map:** every non-trivial URL change indexed in `redirects.json`, validated against a production load test. 3. **Canonical URLs:** explicit canonical on every page, reason: Shopify used query params (?variant=), the new stack may use path-based URLs — canonical prevents duplicate content penalties. 4. **Structured data continuity:** Product schema (offers, availability, price, sku) parity with the original Shopify. Test via Google Rich Results Test before cutover. 5. **Open Graph migration:** og:image, og:title, og:description per page. Dynamic OG image generation via a Next.js dynamic route + edge runtime. 6. **robots.txt:** preserve noindex for admin, account, cart, checkout. Add the `Sitemap:` directive. 7. **Internal linking:** breadcrumbs, related products, category navigation. Spider crawl via Screaming Frog to confirm no page is an orphan. 8. **Core Web Vitals:** LCP, FID/INP, CLS target for the top 50 landing pages. Lighthouse CI in the deployment pipeline. 9. **Search Console reverification:** new property for new infra (if DNS doesn't change, no new verification is needed, but a URL property is recommended). 10. **Monitoring:** 30-day post-launch watch for indexing status, ranking changes on the top 100 keywords, organic traffic delta.
ROI cutoff — when migration is worth it
A headless migration of this scope costs **40–80 k EUR** in engineering (including internal time + external dev shop), plus a 4-month timeline.
**Payback over 3 years:**
| Benefit | Annual saving | |---------|---------------| | Shopify Advanced + Apps savings | 4,200 EUR | | LCP improvement → +6% conversion | 12k SKUs × 20 EUR AOV × 1.2k orders/month × 0.06 = ~17,280 EUR | | Eliminated Shopify Plus upgrade (would be next, 25k+ USD/year) | 23,000 EUR | | Faster B2B quote handling | 12,000 EUR (1 FTE day × 24 months) | | **Total 3-year savings** | **170,940 EUR** |
ROI: ~2.5 years on a 60k EUR investment.
**Who it doesn't pay back for:** - < 2,000 SKUs, < 500 orders/month → Shopify stays cheaper - No dev team or committed external partner for 3-year maintenance - The roadmap doesn't mention B2B / multi-storefront / custom checkout
Practical advice
Migration isn't "we buy Medusa, the developers will do it." It's a **business + tech + SEO + ops project** for 3–6 months. The biggest failures we've seen:
1. **Underestimated EU compliance.** SK invoicing, OSS VAT, GDPR — they all eat more time than the PoC with test data promises. 2. **No 301 plan = lost rankings.** 90 days of organic traffic loss = 30–50% revenue loss. One bad cutover is enough. 3. **No customer support runbook.** Cutover without a 24/7 incident plan = panicked rollback to Shopify after the first crisis. 4. **Underestimated data migration.** 47k historical orders is not a "CSV import over the weekend."
---
*We do headless commerce migrations for 2k–50k SKU catalogues, full-stack (Medusa.js / Saleor + Next.js + integrations + SEO continuity). If you're considering leaving Shopify or BigCommerce, the first project assessment (a 4-hour workshop) walks through stack decision, timeline, budget and the 301 redirect plan for your specific catalogue before you commit to migration.*