We're Leaving Shopify Behind (Again). Here's the Migration Playbook.


We are migrating TonyRobbins.com from Shopify Storefront API to native Stripe checkout. Not because Shopify is bad. Shopify got us here. But we outgrew what it can do for us, and every month we stay on it costs us in flexibility, data ownership, and margin.
This is not a weekend project. This is a multi-phase migration of a production e-commerce system that processes real transactions every day. We cannot break checkout. We cannot lose orders. And we cannot do a big-bang cutover because the blast radius is too large. So we built a playbook.
Why We Are Leaving Shopify
Shopify Storefront API is excellent for storefronts that fit the Shopify model. Ours does not. We sell events, digital products, memberships, and coaching packages. Shopify's product model was never designed for this. Every product is a workaround. Every checkout flow is a hack on top of a hack.
The specific pain points: Shopify takes a transaction fee on top of Stripe's fee. Product data lives in Shopify when it should live in our CMS. Cart state is managed by Shopify's API when we need it in our own state management. Checkout customization is limited to what Shopify allows. And discount logic is constrained to Shopify's model instead of our business rules.
Moving to native Stripe gives us full control over the checkout experience, direct payment processing without middleman fees, product data in Sanity CMS where our content team already works, and discount logic that matches our actual business model.
The Master Flag Pattern
The entire migration is gated behind a single GrowthBook feature flag called native-commerce-enabled. Every piece of new code checks this flag. Flag off means byte-identical behavior to today. Flag on means the new Stripe-native path. This is not optional. This is the architecture.
The flag is resolved once in the root layout and passed down via a React context provider. Every hook and component that needs to branch reads from this provider. There is no prop drilling, no duplicate flag checks, and no way to accidentally mix Shopify and Stripe code paths.
The 8 Phases
Phase 1 is foundation. Master flag, native cart types alongside the Shopify shape, cart API route stubs that return 501 Not Implemented, and module declarations to keep TypeScript happy. This ships first because everything else depends on it. Nothing is user-visible. Nothing changes behavior.
Phase 2 is the cart facade. A useCart() hook that returns a unified CartView regardless of whether the backend is Shopify or Stripe. The facade branches on the master flag internally. Consumers do not know or care which backend is active. This is where the abstraction layer lives.
Phase 3 wires the checkout route. A new /checkout page under the (lp) route group that reads from the native cart when the flag is on. It wraps the existing CheckoutOrchestrator with Stripe Elements. Flag off redirects to the existing Shopify checkout path.
Phase 4 swaps the catalog. Product pages, collection pages, and search all switch from Shopify Storefront API to GROQ queries against Sanity CMS. A catalog facade branches on the flag. The GROQ fetchers normalize Sanity data into the same shape the Shopify fetchers return. Same types, same interfaces, different data source.
Phase 5 verifies discount logic. The existing SKU price-map discount system needs to work identically with Sanity-sourced products. Phase 6 optionally syncs products to Stripe's Products API. Phase 7 runs Playwright snapshot tests comparing flag-on and flag-off rendering pixel by pixel. Phase 8 deletes all Shopify code.
The Snapshot Parity Gate
Phase 7 is the safety net. Playwright tests render every product page, collection page, and checkout flow twice: once with the flag off (Shopify path) and once with the flag on (Stripe path). The screenshots are compared pixel by pixel. Any visual regression fails the build. This is how we know the migration is safe before we flip the flag for real users.
Where We Are Today
Phases 1 through 4 shipped today. Nine PRs merged in one day. The master flag is live, the cart facade works, the checkout route exists, and the catalog consumers are reading from the GROQ facade. Everything is behind the flag, which is off by default. No user sees any difference.
The next step is wiring the 501 cart stubs to the Experience API for real cart persistence, running the discount verification suite, and building the Playwright parity gate. Once all three are green, we flip the flag for internal testing, then gradually roll out to production traffic.
Lessons So Far
The single most important decision was the master flag. Without it, we would be doing a big-bang migration that requires everything to work at once. With it, we can ship incrementally, test in production with the flag off, and roll back instantly if something breaks. If you are planning any large migration, start with the flag.
The second lesson is that facade patterns are worth the abstraction cost when you know both sides of the facade will exist simultaneously for months. The useCart() hook, the catalog facade, and the cart fetcher branching all follow the same pattern: unified interface, flag-based routing, identical output shape. Consumers never change. Only the backend changes.
The third lesson is to ship the stubs first. 501 Not Implemented routes are ugly but they unblock everything downstream. The cart facade hook, the cart fetcher branching, and the checkout route all needed the native cart API routes to exist before they could be wired. Shipping stubs on day one meant four other PRs could merge the same day.
For the original decision to leave Shopify, read Building Ecommerce with Sanity: Why We Are Leaving Shopify Behind. For how we think about checkout as a conversion engine, read Stripe Checkout: From Payment Processing to Conversion Optimization.
