← Blog

Migration

Van Magento 1 naar Medusa 2: shadow-cutover in zes weken

Een Maastrichtse orthopedieleverancier had 9.400 custom protheseconfiguraties vast in Magento 1.9. We zetten ze in zes weken over naar Medusa 2, zonder één order te verliezen.

Jacob Molkenboer· Oprichter · A Brand New Company· 3 dec 2025· 10 min
Open leren grootboek met messing label, groen lint, sleutel en indexkaart op ivoorpapier in zacht licht.

De HEAD-response op de storefront van de Maastrichtse leverancier serveerde nog steeds een Magento 1.9-footer. PHP 5.6 in de response headers. End of life sinds juni 2020 voor het platform, december 2018 voor de runtime. 22 mensen in het bedrijf, 9.400 custom protheseconfiguraties achter de B2B-login, en een portal waar 180 ziekenhuizen en orthopedische klinieken in de Benelux elke dinsdag op rekenden om de inkooporders van de week in te dienen.

De site is zes jaar lang gepatcht, monkey-patched en backported. De laatste security-audit adviseerde één ding: stop met patchen, begin met verhuizen.

Dit is de playbook waarmee we ze in zes weken hebben verhuisd zonder een order te verliezen.

De audit voor de audit

Voor we ook maar één architectonische beslissing namen, hebben we vier dagen lang de codebase gelezen. Niet gedraaid. Gelezen.

Magento 1.9-sites die tien jaar oud zijn, zijn nooit puur Magento. De portal van de orthopedieleverancier had:

  • 41 custom modules onder app/code/local, waarvan er maar zeven een werkende test hadden.
  • Een ProthesisConfigurator-module die configuraties wegschreef naar zeven custom MySQL-tabellen buiten Magento's EAV om.
  • Een nachtelijke cron om 02:30 die inkooporders uit AFAS Profit ophaalde via SOAP.
  • Een prijsafspraak-engine per klant die de tier pricing van Magento overschreef met een prijsindex, geladen uit een CSV die het salesteam uploadde.
  • 14 hardcoded SKU's in de checkout die de oorspronkelijke developer (vertrokken in 2017) rechtstreeks in Mage_Sales had ingebouwd.

Je kunt niet migreren wat je niet begrijpt. We hebben elk custom write-pad in kaart gebracht voor we ook maar één config-bestand aanraakten.

Waarom Medusa 2, niet Shopify B2B

We hebben drie opties overwogen.

Shopify B2B Plus was snel beoordeeld en snel afgewezen. 9.400 SKU-varianten zit boven het harde varianten-plafond van Shopify, zelfs met metaobjects, en de prijslogica per klant zou bij elke cartberekening in Shopify Functions gaan draaien. De AFAS-bridge zou op een Cloudflare Worker leven, met een extra network hop die de IT-lead van de leverancier in het eerste gesprek al vetode.

BigCommerce B2B Edition was de runner-up, maar hun headless verhaal loopt nog steeds via de BigCommerce-checkout. Het compliance-team had die optie al geschrapt: AFAS Profit moet een factuurnummer uitgeven voor de checkout afrondt, en bij BigCommerce kun je de checkout niet blokkeren op een externe call.

We kozen Medusa 2: een Node.js commerce kernel die in je eigen stack draait. Je bezit de database. Je schrijft modules in TypeScript die lijken op de modules die de vorige developer in PHP schreef, alleen dan fatsoenlijk. Het praat Postgres, komt met een workflow engine, en de storefront is gewoon Next.js. Klaar.

Week één, de data spine

De grootste fout die teams maken bij dit soort migraties: data behandelen als een eenmalige import. Dat is het niet. Zes weken lang moeten beide stacks dezelfde source of truth lezen en schrijven, anders valt iemands order tussen wal en schip.

We zijn begonnen met Postgres naast de bestaande MySQL te zetten. Zelfde VPC, zelfde security group, schone lei. Daarna bouwden we een one-way replicator die CRUD-events oppikte uit de binlog van MySQL via Debezium, elke rij omzette naar de Medusa-vorm en wegschreef naar Postgres.

Inkooporders, configuraties, klanten, prijsafspraken, alles.

Aan het einde van week één liep de Postgres-kopie zo'n 45 seconden achter, wat prima is voor alles behalve checkout-writes. Checkout was het probleem van week twee.

Week twee, de checkout dual-writen

Nu het gevaarlijke deel. De checkout moest óók naar Postgres gaan schrijven, zodat orders op de verouderde portal ook in Medusa bestonden.

We deden dat door het sales_order_save_after-event van Magento te onderscheppen met een kleine custom module, en een genormaliseerde payload te posten naar een Node.js-bridge op dezelfde machine:

<?php
class OrtoSync_Bridge_Model_Observer
{
    public function salesOrderSaveAfter(Varien_Event_Observer $observer)
    {
        $order = $observer->getEvent()->getOrder();
        if ($order->getOrigData() !== null) {
            return; // only on first save, never on updates
        }
        try {
            $payload = Mage::helper('ortosync')->normalizeOrder($order);
            Mage::helper('ortosync/bridge')->post('/orders', $payload);
        } catch (Exception $e) {
            Mage::log($e->getMessage(), null, 'ortosync.log');
            // never throw. the legacy order must save no matter what.
        }
    }
}

De bridge accepteerde de payload, draaide hem door de order workflow van Medusa en tagde het resultaat met een magento_order_id. Als Medusa hem om welke reden dan ook afwees (validatie, ontbrekende klant, wat dan ook), logde de bridge dat en sloeg de Magento-order alsnog normaal op. Geen regressierisico voor de live business.

Aan het einde van week twee bestond elke nieuwe order in beide databases. We zagen het aantal afwijkingen in vier dagen tijd dalen van 19 per dag naar nul, terwijl we de mismatches één voor één oplosten.

Waarschuwing

Laat je dual-write nooit hardop falen aan de verouderde kant. De nieuwe stack moet zijn eigen fouten opvangen. Als je oude checkout 500's gaat retourneren omdat het nieuwe systeem ontevreden is, heb je een outage gebouwd in plaats van er een voorkomen.

Week drie, de AFAS Profit-bridge

De nachtelijke inkooporder-sync was het risicovolste stuk. De SOAP-connector van AFAS Profit doet het prima als hij het doet, maar de verouderde cron job retryde fouten oneindig, wat in 2022 ooit de AFAS-instance van de leverancier had overspoeld met 14.000 dubbele orders tijdens een netwerkdip.

We schreven hem opnieuw als Medusa-workflow met drie stappen:

  1. Haal de inkooporders van de dag op uit AFAS via de Get connector over REST, niet SOAP. AFAS leverde REST-endpoints in 2023 en die zijn strikter en sneller.
  2. Match elke order tegen de order-tabel van Medusa op purchase_order_number.
  3. Markeer mismatches voor menselijke review in het admin-panel, in plaats van te retryen.

De workflow draaide twee weken parallel met de verouderde cron. We vergeleken elke ochtend de output. Drieënveertig discrepanties kwamen boven. Eenenveertig waren de oude cron die het verkeerd deed (AFAS-validatiefouten stilzwijgend wegslikte). Twee waren onze nieuwe code. Die hebben we gefixt.

Week vier, de configurator

De ProthesisConfigurator was het hart van het systeem. Een klant kiest een basisprothese, maakt vervolgens tussen de vier en drieëntwintig keuzes (materiaal, gewrichtstype, koker, alignment, cosmetische afwerking), en het resultaat is een SKU die soms al in de catalogus bestaat en soms on the fly gegenereerd moet worden.

De verouderde implementatie zat in een PHP-class van 2.400 regels die uit die zeven custom tabellen las. We hebben hem herbouwd als Medusa-module:

import { MedusaService } from "@medusajs/framework/utils"
import { Configuration, Variant } from "./models"

class ConfiguratorService extends MedusaService({
  Configuration,
  Variant,
}) {
  async resolveConfiguration(input: ConfigInput): Promise<string> {
    const fingerprint = hashChoices(input.choices)
    const existing = await this.retrieveVariantByFingerprint(fingerprint)
    if (existing) return existing.id

    const created = await this.createVariant({
      product_id: input.base_product_id,
      sku: buildSku(input.base_sku, fingerprint),
      metadata: { fingerprint, choices: input.choices },
    })
    return created.id
  }
}

De module exposeert één resolveConfiguration-call die de Next.js-storefront via de Medusa-API aanroept. Die geeft of een bestaande variant-id terug, of genereert een nieuwe en registreert die als een echte Medusa product variant voor hij terugkeert.

De 9.400 historische configuraties zijn in een eenmalig script geïmporteerd als Medusa-varianten. Elke configuratie behield zijn originele SKU, zodat reorder-flows van klanten bleven werken zonder SKU-vertaallaag.

Week vijf, prijsafspraken per klant

De CSV-upload-pricing engine was het onderdeel dat we het meest verleid waren om te herschrijven. Niet gedaan. Geleerd van eerdere migraties: als sales een workflow heeft die werkt, vervang je die niet tijdens een tech-migratie. Je vervangt de techniek en laat de workflow met rust.

We bouwden een Medusa Price List-module die exact het CSV-formaat accepteerde dat het salesteam zes jaar lang had geüpload. Zelfde kolommen, zelfde encoding, zelfde dubieuze behandeling van komma's binnen geciteerde velden. Het enige verschil was dat de upload nu in S3 belandde en een workflow triggerde die hem diffte tegen de huidige prijslijst voor toepassing.

Het salesteam hoefde niets aan te passen. Ze merkten het wel, want de uploadpagina was sneller.

Week zes, de cutover

Op maandag van week zes ontving Medusa al drie weken lang elke write. Postgres en MySQL zaten op elke relevante tabel binnen seconden van elkaar. De Next.js-storefront stond tien dagen live op een staging-subdomain. Twaalf interne gebruikers hadden testorders geplaatst. Het salesteam had drie echte prijslijsten in het nieuwe systeem geüpload.

We cutoverden op donderdag om 14:00 CET. Niet vrijdag. Nooit vrijdag.

De DNS-flip wees orders.example.nl naar de Next.js-storefront. De Magento-storefront bleef nog een week draaien op legacy.orders.example.nl als terugvaloptie. We keken 90 minuten naar de logs. Twee klanten liepen tegen een bug aan waarbij de cart hun prijsafspraak niet liet zien (een cache key-collisie die we in staging hadden gemist). In elf minuten gepatcht. Geen orders verloren.

Vrijdagochtend was de verouderde bridge uitgezet. De woensdag erna was de oude VPS gearchiveerd. De dual-write-infrastructuur draaide nog drie weken door als goedkope verzekering.

Wat we anders zouden doen

Twee dingen.

Eén: we hebben onderschat hoeveel tijd de edge cases van de configurator zouden kosten om uit te spoelen. Tussen de 9.400 configuraties zaten er 47 die in 2018 handmatig in de database waren aangepast om een factureringskwestie op te lossen, en die nooit opnieuw via de UI waren opgeslagen. Die importeerden als kapotte varianten. We hebben ze gevangen, maar alleen omdat een salesrep in de testfase een SKU zag renderen zonder afbeelding. Bouw een configuratie-integriteitscheck voor je importeert, niet erna.

Twee: de AFAS REST-migratie had los van de storefront-migratie kunnen plaatsvinden. We bundelden ze omdat we één cutover wilden, maar achteraf had de REST-switch zes maanden eerder kunnen shippen zonder enige storefront-impact, en had hij de grotere move minder risicovol gemaakt.

Toen we deze legacy-migratie voor de orthopedieleverancier deden, was de cache key-collisie op prijsafspraken het stuk dat ons bijna heeft genekt (Magento hashte klant plus SKU, onze Next.js-laag keyde op klant plus variant_id, en die twee liepen uiteen voor configurator-output). We vingen het in productie, en sindsdien logt elke dual-write-bridge die we bouwen de cache key naast de order payload.

Het kleinste nuttige dat je na het lezen hiervan kunt doen: open de HEAD-response van je Magento 1-site en kijk naar de X-Powered-By-header. Staat er PHP 5.6 of PHP 7.0, dan draai je op een runtime die al jaren uit security support is. De migratie is het project. Weten welke runtime je klanten exact bedient is een audit van vijftien seconden die je vandaag kunt doen.

Kern

Met shadow-traffic-cutovers vang je de stille AFAS-retries en cache key-collisies voor je klanten ze voelen, niet erna.

FAQ

Waarom kiezen voor Medusa 2 in plaats van Shopify B2B voor zo'n portal?

Het variantenplafond van Shopify en de checkout-lock-in waren beide blokkers. De configurator genereert SKU's on demand, en AFAS moet factuurnummers uitgeven voor de checkout afrondt. Met Medusa houd je de kernel en de checkout flow in eigen beheer.

Hoe lang duurt een Magento 1-migratie realistisch?

Voor een custom B2B-portal met dit integratie-oppervlak is zes engineering-weken plus twee weken buffer de ondergrens. Schonere codebases halen er twee weken af. Sites met meer custom modules hebben twaalf weken nodig.

Zijn de kosten van shadow traffic draaien het waard?

Ja. De discrepanties die het bovenhaalt zijn precies degene die op cutover-dag een outage hadden veroorzaakt. Twee weken dual-writen verdient zich terug zodra het de eerste stille AFAS-retry of cache key-collisie vangt.

Wat is het grootste risico tijdens de cutover zelf?

Cache keys. Verouderde systemen hashen dingen op manieren die niemand zich nog herinnert. Wij loggen de cache key altijd naast de order payload, zodat elke afwijking zichtbaar is op het moment dat hij optreedt, niet pas wanneer een klant klaagt.

migrationmagentolegacy sitese-commercephparchitecture

Iets bouwen?

Start een project