Headless WooCommerce: The Architecture Decisions That Actually Matter
The Question Behind the Question
"Should we go headless?" is rarely the right question. The right one is: what does the existing WooCommerce stack cost us that a headless rebuild would fix? If the answer is "the storefront is slow", that's solvable inside WordPress with caching and a CDN — no rebuild needed. If the answer is "we want to ship the same catalog to a mobile app, a kiosk, and a marketing site", headless starts to earn its keep.
Assuming the rebuild makes sense, here are the architecture decisions that determine whether the project ships in three months or twelve.
Decision 1: REST vs GraphQL
WooCommerce ships with a mature REST API. WPGraphQL + WooGraphQL adds a GraphQL layer on top. Both work; the choice depends on your team and your read patterns.
Pick REST when: the storefront does mostly category and product page fetches, your team has no GraphQL experience, or you want to avoid the N+1 query performance trap that bites unoptimized GraphQL resolvers under load.
Pick GraphQL when: the frontend needs to compose data from products, categories, posts, and ACF fields in a single request, or when you're already running GraphQL elsewhere in the stack.
On a recent project, I migrated from GraphQL to REST mid-build. The reason: WPGraphQL's variable-product queries were generating 80+ SQL queries per request, and the fix required patching WooGraphQL's resolvers. Falling back to two REST calls plus client-side composition was faster to ship and easier to cache at the CDN edge.
Decision 2: Cart on WordPress or Cart on Frontend
This is the decision that defines the rest of the project. Every other architecture choice bends around it.
Cart on WordPress (CoCart, Store API): sessions, shipping, taxes, coupons, payment all stay in WooCommerce. The frontend is a view layer. Pros: zero risk of cart divergence from order; all extensions (subscriptions, bookings, bundles) work; payment methods stay PCI-scoped to WordPress. Cons: every cart interaction is a network round-trip to WordPress, and WordPress becomes a hard runtime dependency for the frontend.
Cart on frontend (custom Stripe/Square integration): the cart lives in the frontend (Zustand, localStorage), and the order is created on WordPress only at checkout completion. Pros: instant cart interactions, frontend can work briefly during WordPress downtime. Cons: you reimplement every WooCommerce cart feature — shipping, taxes, coupons, stock validation — which is months of work and a permanent maintenance tax.
For 90% of projects, cart-on-WordPress via Store API is the right answer. The latency cost is real but manageable with optimistic UI updates. Reimplementing WooCommerce's cart logic is a trap.
Decision 3: ISR, SSR, or Static
Next.js gives you the full spectrum. The pattern I've settled on for medium catalogs (under 50k SKUs):
- Static + revalidate: product pages, category pages, marketing pages.
generateStaticParamsfor top-100 products, ISR with a 5-minute revalidate window for the long tail. - Server-rendered: search results, account pages, anything authenticated.
- Client-side: cart, mini-cart, recently viewed.
The static-with-revalidate pattern is what makes headless feel fast. A product page that's been generated once serves from the CDN in <100ms regardless of WordPress load. The 5-minute window means stock counts may be slightly stale, but a webhook from WooCommerce on stock change triggers an on-demand revalidate within seconds for popular SKUs.
Decision 4: Hosting Split
The frontend wants edge caching, instant deploys, and global distribution — Vercel, Netlify, Cloudflare Pages. WordPress wants a beefy origin with predictable database performance — Kinsta, WP Engine, or a tuned VPS. Putting them on the same host optimizes for neither.
The split I use: WordPress on Kinsta (admin + REST/GraphQL endpoint), Next.js on Vercel (frontend), Cloudflare in front of both. WordPress hostname is treated as an internal API; the public domain points at Vercel.
Decision 5: How Authentication Actually Works
WooCommerce assumes WordPress sessions — a PHP session cookie, set by WordPress, read by WooCommerce. Headless breaks this assumption.
The pattern that works: JWT auth via the jwt-authentication-for-wp-rest-api plugin (or a custom equivalent), with the JWT stored in an HttpOnly cookie set by the Next.js server. The frontend forwards this cookie on every API call; WordPress validates and returns the user-scoped data.
For checkout specifically, CoCart's cart-key-based guest session is essential. New visitors get a cart key on first add-to-cart, and that key persists in localStorage until login, at which point the guest cart merges into the user cart.
Decision 6: What the Migration Looks Like
You don't rebuild a live store overnight. The pattern I've used twice now:
- Subdomain launch: ship the headless frontend to
new.example.com. Both stores share the same WooCommerce backend, so orders flow into the same admin. - Soft migrate top SKUs: redirect the top 50 product URLs to the new frontend. Monitor conversion, fix what breaks.
- Full cutover: swap DNS, set up 301 redirects for any URL structure changes, keep WordPress at
shop.example.comfor admin.
The "shared backend during migration" approach lets you ship in stages without holding two databases in sync. The admin team keeps working in WordPress; the storefront just changes which frontend renders the catalog.
The Operational Reality
Headless WooCommerce adds runtime dependencies. A broken WPGraphQL plugin update can take down your frontend. A misconfigured CDN can serve stale stock. A WordPress migration with a different domain breaks every permalink field in the frontend cache.
The fix is monitoring and graceful degradation: health checks on the WordPress endpoint, fallback static catalog snapshots for true outages, and a frontend that can show "checkout temporarily unavailable" without 500-ing the whole page. None of this is glamorous, and all of it is what keeps the architecture viable in production.
