Case Study · Sunny Sahijwani

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

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.

Client anonymized throughout. I'm happy to walk anyone evaluating my work through the full project — architecture, decisions, and numbers — in a personal conversation.

More case studies