Replatforming a live 550K+ order D2C store from Shopify to a custom Laravel 11 + Next.js stack — without stopping sales
As technical lead and sole backend/infrastructure engineer, I moved a European D2C fragrance brand off Shopify onto a custom Laravel 11 + Next.js stack — 8 markets, multi-currency, in-house payments, logistics, loyalty, and analytics — with the first market cutover delivered on the planned date in June 2026, and the store taking orders continuously throughout the migration.
Context
The client is a D2C fragrance brand selling into Germany, Switzerland, and six other European markets. The business ran entirely on Shopify plus a stack of SaaS subscriptions (Smile.io loyalty, Shopify Flow automations, the Shopify–Klaviyo connector, a legacy standalone label app). I joined in September 2024 for Shopify theme and Klaviyo work (Sept 2024 – Aug 2026, ~2 years); the mandate later grew into leading a full replatform to an owned stack.
Challenge
Replatform a store with 550K+ lifetime orders without a freeze: checkout, fulfillment, loyalty points, discount codes, and marketing automations all had to keep working every day. I owned architecture, data migration, launch planning, and production operations end to end as the sole backend/infrastructure engineer; in the final phase, frontend contributors were onboarded under my direction, and I trained a successor as part of a structured handover.
What I built
A sync bridge first, so the new system ran in shadow with real data. 11 Shopify webhooks feeding async Horizon queue jobs, plus a 6-hourly reconciliation command for gap detection, plus a historical backfill of 217K+ orders back to December 2019 — the operationally relevant scope for shipping, loyalty, and reporting; the rest of the lifetime ledger was covered by the live webhook bridge and subsequent coverage passes. The shadow period paid off before launch: I discovered and recovered a ~70K-order sync gap (≈12% of lifetime orders), including a large slice of a peak-season sales window, by building GID-level coverage tooling against a 4GB Shopify bulk export — the production backfill ran clean in ~6 hours with 4 parallel workers. I also cleaned 509 duplicate order pairs with a money-trail-aware dedupe and closed the race permanently with a unique index.
flowchart TB
A["Phase 1 — Shadow run: 11 webhooks + 6-hourly reconciliation feed real production data into the new stack (no customer traffic)"] --> B["Phase 2 — Backfill: 217K+ historical orders + recovery of a ~70K-order sync gap"]
B --> C["Phase 3 — Market-by-market cutover: runbooks, rollback triggers, one-line toggle-off"]
C --> D["Phase 4 — Apex-domain cutover: the custom stack becomes primary"]
Migration strategy: the new platform proved itself on real data before any customer touched it.
An 8-market, multi-currency storefront sized to the actual business. Instead of cloning Shopify Markets, I analyzed the real order split (the overwhelming majority of orders settle in EUR — roughly ~87% — with CHF a small fraction) and shipped a simpler per-country-subdomain model: subdomain → currency + language, with settlement-currency capture on payments and idempotent backfill commands. Nine storefront hostnames, German/English/French/Italian/Spanish/Swedish/Danish variants, and a design that adds a new market with one row instead of a model rebuild.
Payments with redundancy built in. Stripe, PayPal, and Unlimit, each with webhook handling and a 6-hourly cron reconciler so a missed webhook can never strand money. Stripe went live on launch morning after an end-to-end test with a real paid-and-refunded production order. Later hardening included partial refunds and a guard preventing checkout-supersede logic from killing orders with in-flight PayPal captures (25 tests / 92 assertions).
Logistics migrated in one 9-phase delivery. DHL (two accounts) + Swiss Post booking, a volume/weight box-assignment algorithm, batch label PDF/ZIP generation for the warehouse, an admin error-triage UI, and a self-healing retry pipeline — 60+ files, 36+ shipping tests, run behind kill switches in manual mode until the legacy label app was decommissioned. DHL Packstation locker support was added end-to-end on the same pipeline.
SaaS replacements de-risked with shadow modes. The Smile.io loyalty program was rebuilt in-house (earning, revocation, VIP tiers, referrals, expiry) using a feature-flagged authoritative mode with diff-logging before cutover — 11 phases, 135 tests green, 44K+ legacy discount codes reconciled. Klaviyo was cut over to a custom catalog integration with all 430 back-in-stock subscriptions migrated intact. Ten transactional email types moved into an admin-editable CMS across 4 locales (58 tests), with a test-mode allowlist so migration imports could never mass-mail real customers.
Launch discipline, not launch hope. A k6 load-test baseline proved ~198 RPS sustained over 24,440 requests with zero HTTP errors and identified PHP-FPM (not the DB) as the scaling ceiling. The Swiss cutover ran off a time-bucketed runbook with explicit rollback triggers and a one-line toggle-off requiring no deploy. Deploys were converted to zero-downtime (graceful php-fpm reload + backwards-compatible-migrations policy), eliminating 90–180 seconds of 503s per deploy.
flowchart LR
U["Customers"] --> CF["Cloudflare (LB / WAF)"]
CF --> FE["Next.js storefronts — 9 hostnames, 8 markets"]
FE -- "GraphQL" --> API["Laravel 11 API"]
SH["Shopify (legacy, during migration)"] -- "11 webhooks" --> Q["Horizon queues"]
SH -. "6-hourly reconciliation + backfill" .-> Q
Q --> API
API --> DB[("MySQL")]
API --> RD[("Redis — cache / queues")]
API --> MS[("Meilisearch")]
API --> PAY["Stripe · PayPal · Unlimit"]
API --> SHIP["DHL · Swiss Post"]
API --> MKT["Klaviyo · Google Merchant"]
Target architecture: one Laravel core owning data, payments, logistics and marketing; Next.js storefronts per market behind Cloudflare.
Results
- First market (Switzerland) launched on the planned date in June 2026; the apex-domain cutover followed ten days later. The old store kept selling throughout the migration.
- Five SaaS dependencies replaced with owned systems (Shopify checkout, Smile.io, Shopify Flow, the Klaviyo connector, the legacy label app).
- Admin order search went from 4s to <50ms; the top storefront GraphQL query from 150–600ms to <10ms via a tag-invalidated Redis cache; management reports read in <8ms off rollup tables covering ~6.5 years of data.
- Analytics shipped with bot filtering after I proved ~23% of recorded sessions were datacenter bots — so the business made decisions on clean numbers.
What I would do differently
Disaster recovery on day one. Production ran on a single hosting provider with no tested failover, and when that provider went completely down in mid-2026 I had to improvise a same-day failover to a standby cloud box. It held — and the next day I led the permanent cutover to that failover environment as the new primary. But that was recovery, not redundancy: multi-provider failover was still an open gap at the time, and I own that. Documented runbooks, tested restores, and a multi-provider posture are now a first-sprint deliverable in how I run infrastructure. I would also push harder, earlier, on third-party access dependencies (payment-dashboard and platform API scopes held by the client), which sat on the critical path longer than they needed to.