Building a headless Shopify store — a field checklist

A production-earned checklist for taking a real headless Shopify store live — architecture, Shopify wiring, a provider-hosted checkout, customer accounts, SEO, and the traps waiting at every seam.

Illustration of stacked translucent layers linked to a glowing sphere, representing a decoupled headless commerce architecture.

Image via Shopify

Distilled from taking a real headless Shopify store to production. Shopify is the constant — the system of record, and the platform every integration lesson here assumes (Storefront + Admin + Customer Account APIs, plus a provider-hosted one-page checkout). The frontend stack is not: pick whatever framework, styling and hosting serve you best — the lessons below are about Shopify and the seams around it, and they survive any rendering layer. Every item either bit us or was designed to prevent something that would have.

The checklist is provider- and market-independent. Wherever it says "your payment provider", read: whichever one-page/overlay checkout you integrate. The traps are structural to the pattern — provider takes payment on your site, you create the Shopify order afterwards — not to any vendor. Concrete examples from our build are marked (ours).

This file is the order of operations and the traps — keep your own reference docs for the detail behind each item.

Written mid-2026 against current Shopify APIs. Some specifics will rot — the patterns won't.

Should you go headless at all?

Ask this before reading further, because the honest industry numbers are sobering: analyst surveys report a majority of adopters citing higher-than-expected implementation cost as their biggest regret, and only a small minority of even large merchants run fully decoupled storefronts. Treat those figures as directional (they're surveys), but the direction is clear — headless is a minority path, chosen on purpose.

Two things changed the math recently: AI-assisted development collapsed the cost of the pages (the seams in this document are what's left to pay for), and the re-skin dividend below compounds if you're building more than one store. If neither applies to you — and this list frightens you — a well-built theme is a legitimate conclusion of reading this document.

Why pay this tuition — the benefits we actually banked

Everything below is a cost. Here is what it bought, each tied to a mechanism — not an adjective:

  • Performance by construction, not by optimization. Static pages with a handful of interactivity islands ship almost no JavaScript. A theme store accumulates an app-script tax — every installed app injects its own JS — that no amount of tuning removes; here the budget is structural. On mobile-first markets this is the difference customers feel.
  • The app-subscription stack becomes code you own. Cart drawer, predictive search, bundle builder, a partner-application form stored as native platform records — each of those is a monthly app subscription (and a third-party script, and a data processor) in a theme store. Built once, they're free forever and render in your design system instead of an iframe.
  • Consistency by construction. The product feed, the structured data, and the product pages all render from the same build-time catalog — so price and availability cannot disagree across surfaces. In a theme+apps stack those are three integrations that drift; here the drift is impossible by design. Same effect site-wide: design tokens and compliance rails live in one place and every surface inherits them.
  • Editorial surfaces themes can't express. A magazine-layout education hub, custom article typography, bespoke landing pages — no theme sections/blocks ceiling, no page-builder app. If you can design it, you can ship it.
  • Checkout on your own page. The provider overlay opens on-site instead of redirecting away — with the full trap list above as the price of admission.
  • The re-skin dividend. Because every page is assembly from tokens + shared components, a second store is the same system with different tokens, copy and catalog — days, not months. This is where the economics compound for anyone building more than one.
  • Full-fidelity instrumentation. You define every event, own the purchase dedupe, and forward the checkout overlay's abandonment events — instead of accepting whatever a theme's analytics app emits.
  • AI leverage. A single legible codebase with documented rules is something AI-assisted development compounds on; a theme + 30 apps is opaque to it. This is the change that moved headless economics since 2024 — the pages got cheap; the seams (this checklist) are what's left to pay for.

Benefits the industry cites that we can't vouch for (yet):

  • Traffic-spike resilience. CDN-served static pages plus small functions should absorb a flash sale or viral moment without the backend feeling it — the mechanism is genuinely built into this architecture, but we haven't had the spike that proves it.
  • Omnichannel reuse. One catalog behind web, native apps, kiosks and marketplaces. Our only second surface so far is a shopping feed — real, but a modest version of the claim.
  • Decoupled team velocity. Frontend deploys that never risk the commerce engine, so releases go from monthly to daily. Plausible; meaningless to verify on a small team.
  • Experimentation/A-B velocity. Cheap iteration on the rendering layer. True in principle — we iterated constantly — but we ran no controlled experiments, so we won't claim the conversion math.

And a caution: vendor case studies in this space quote content-velocity and conversion multipliers ("7×", "50% faster") that we found no independent verification for. Distrust benefit stats from companies selling the architecture — the mechanisms above stand on their own.

One honesty note: these benefits accrue only to a finished build. Half-built headless is the worst of both worlds — theme constraints traded for integration debt. That's what the rest of this document is for.


0) Architecture decisions — settle these before writing code

  • Static-first, islands for interactivity only. For a small catalog + editorial content, ship ~zero JS by default (islands: cart, buy-box, search). A mid-range phone on a patchy network is the median customer in most markets. A site-wide scroll-reveal that pre-hid content behind JS caused a client complaint from the live deploy and was ripped out — decorative motion must never gate content. Scope check: build-time catalog baking works for a small catalog; at thousands of SKUs the strategy changes (on-demand rendering/ISR), and this whole checklist assumes the small case.
  • Decide where editorial content lives — it's a fork, not a default. Headless guides assume you'll add a headless CMS; we deliberately didn't. Editorial copy lives as versioned data files in the repo: reviewable in pull requests, greppable, AI-legible, no vendor, and impossible to drift from the code that renders it. The honest cost: every copy edit is a developer edit — a client who wants to change a headline needs you. Pick per project: content-heavy brands with in-house editors want a CMS (we've used Sanity.io for that); a one-or-two-person team or a small-catalog store on an agency retainer is usually better off with content-as-code. Deciding late means migrating content twice.
  • Decide who the system of record is, and never fork it. Ours: Shopify owns orders/inventory/customers; local data files own editorial copy; a build-time merge overlays Shopify price/variant/image. Pages call the merged getters, never the raw local data.
  • Map the full page inventory and URL structure before building any page. Mirror Shopify's non-removable prefixes 1:1 (/products, /collections, /pages, /blogs/{blog}/{article}) so handles map cleanly when Shopify connects; document the deliberate exceptions (utility routes like /search, /account, a short link-in-bio route) with their reasons, or every future contributor relitigates them. Kill legacy flat paths (/shop, /about) early and crawl-test that nothing links to them.
  • Know your Shopify plan's API limits before designing. ⚠️ Basic plan blocks the Admin API from the Customer object entirely (PII is Shopify/Advanced/Plus only). Anything needing customer names/emails/ addresses server-side must run on the Customer Account API with the customer's own token. This shaped our whole account centre — discover it before you design, not after.
  • One storefront, one domain. If the Shopify Online Store theme stays reachable, you have two storefronts for one catalog — and Google Merchant Center will eventually notice (§8 below). Plan the lockdown from the start.

1) Design system — lock it before the first page

Pages built before the system exists get rebuilt. Lock these, then produce pages against them.

  • All tokens in one place (colors, spacing, type, radii — ours: a single global stylesheet), and a hard rule: never a raw hex or arbitrary value in a component when a token exists. Per-entity colors (our per-SKU panel/accent pairs) live in the data file next to the entity, so they flow everywhere from one definition.
  • An accessibility variant for small text, from day one. Our brand green measured 4.44:1 on white — fails AA for small text — so a darker text-brand-ink token exists specifically for text under ~18px. Decide this pair when you pick the palette, not when an audit fails a nav link.
  • Vendor the full icon set once, one weight, lazy-loaded (ours: a 1,500-icon open-source set, loaded so only referenced icons build). The alternative — copying icons in one at a time — produced hand-drawn approximations and mixed weights until we banned it. If you render icons through more than one path (server components and client islands), both must read the same source files, or the sets drift.
  • Normalize scales early: one section-spacing scale (ours: a single standard section padding, page-top heroes slightly deeper), one product-media radius scale (large media vs small thumbs). We did this normalization after pages existed and it touched everything; do it before.
  • Document deliberate exceptions inside the system. Our band system (white pages + one tint) has exactly two exceptions (announcement strip, one menu chip) — written down as exceptions. An undocumented exception reads as permission.
  • Motion: start restrained, and let performance veto aesthetics. We trialled a heavier direction (animation library, serif accents, decorative motifs) and pulled it all back; the surviving stack is page-transition cross-fades + CSS micro-interactions, honoring prefers-reduced-motion. Whatever you keep: content must render without JS, and "cool" never outranks a mid-range phone.
  • Mockups are design reference, not code to copy — look and structure only. Treating comps as literal markup imports their inconsistencies; extracting them into tokens + components is the actual job.
  • Build the shared component inventory before the pages that use it (container, button variants, eyebrow/kicker, product card, post card, stars, icon). Every page built afterwards is assembly; every component invented mid-page is a future refactor.

2) Content & page production process

  • Ground the brand voice in the founder's own words before writing copy. Our first voice doc was AI-drafted plausible fluff; v2 was rebuilt from a founder questionnaire, every claim provenance-tagged, with an approved rewrite table. Do the interview first — it's the difference between copy the client recognises and copy they tolerate.
  • Copy is sourced, never invented. From mockups, briefs, or the client — verbatim. If a slot has no copy, use an obvious placeholder and ask. And don't invent a tagline — ours was retired because it was never approved, just plausible.
  • A "say it once" pass: one claim moment per page. Our core message appeared in the hero, the eyebrow, the cards and the footer of the same page until a dedicated de-duplication pass; eyebrows only where they add information beyond the heading.
  • No fabricated social proof — ever. We shipped placeholder star ratings, review quotes tagged "Verified", and a five-star "loved by our first customers" card, then had to hunt them all down and delete them pre-launch. Placeholder ≠ fake: a missing reviews section is honest, an invented one is a liability. Wire a real review app before launch instead.
  • Label interim assets as interim, in writing (docs + a manifest). All our imagery was style-reference placeholder until a real shoot; the docs say so on every surface, which is what stopped it being mistaken for final — or worse, used as the source for generated imagery.
  • Regulated-category compliance rails, written down before copy is written (ours: food/nutrition — no medical claims, certification claims only on the certified SKU, per-serving numbers only, one scientifically-wrong-but-common category claim explicitly banned with the mechanism documented). Every marketing surface inherits the rails; "the category does it" is how the claim got there.
  • Legal pages as early drafts (privacy, terms, shipping/returns) flagged for client review — they take one pass to draft and weeks to get reviewed, so start the clock early. Keep operator identity consistent and minimal.
  • Be willing to revert. Our history contains a five-commit saga iterating decorative arrows that ended in "remove them entirely", a layout experiment reverted same-day, and a pull-quotes feature reverted because its sources couldn't be verified. The revert commits carry the reason — which is what stops the idea being re-tried in six months. Features must earn their keep; sunk iteration is not an argument.
  • Docs-as-you-go, two tiers: a short operating file for rules and constraints (which is also what makes AI-assisted sessions productive instead of re-explaining the repo every time), and a full reference recording decisions with reasons, corrections when claims turn out wrong, and a copy-debt list for known not-yet-migrated content.
  • Per-change verification loop, cheap enough to actually run: dev server + typecheck at 0 errors on every change; screenshot verification at a mobile and a desktop viewport for anything visual; full production build only at deploy time (ours runs only alongside a push — running it per edit was pure friction).

3) Shopify wiring

  • Storefront API with a private token, server-side only — it bypasses the dev-store password gate and never reaches the browser. Browser → your own /api/cart proxy → Shopify. Never trust client-supplied totals or IDs.
  • Build-time catalog fetch + a rebuild webhook. Prices/images bake at build, so admin edits need a redeploy: Shopify products/* webhook → HMAC verify (constant-time) → deploy hook. Two production lessons: debounce ~10 min (one multi-item order fires one inventory webhook per product) and ack failures with 200 (non-2xx just makes Shopify retry-hammer).
  • Separate tokens, separate jobs. Storefront token (catalog/cart) ≠ Admin token (order creation). Check scopes with { currentAppInstallation { accessScopes { handle } } } before assuming a scope is missing — changing a custom app's scopes forces a reinstall and rotates the token that creates every paid order.
  • Never ship full-res commerce images. Append the image CDN's width parameter everywhere an image renders — cards, gallery thumbs, checkout line-item metadata, merchant feeds (which also have minimum sizes). One tiny helper, used at every call site; grep for bare image URLs in review.
  • Free-shipping threshold: read it from the delivery profile at build, don't hardcode. Static copy that quotes the number must be grepped when the threshold changes — list those spots in your docs.
  • Pin the API version — then diarise its funeral. Shopify cuts a new API version quarterly and supports each for ~12 months; a pinned version rots silently until requests fall forward onto a version you never tested. Put the upgrade on a recurring calendar entry, read the deprecation changelog each quarter, and record which version every client/token in the codebase pins.
  • Know your oversell window. Availability baked at build means a sold-out product still shows buyable until the next rebuild — the rebuild webhook shrinks the window but doesn't close it. Decide deliberately: check live availability at order creation, or accept the window and set the inventory policy so order creation respects stock levels rather than decrementing blindly.
  • Merchant-owned metaobjects for site-generated records (ours: affiliate applications): reviewable in admin, no new vendor. Note: Shopify rejects an access block on merchant-owned definitions ($app: types only), and merchant-owned is usually what you want anyway — records not bound to one app.

4) Cart & checkout UX

  • Cart state in a tiny persisted client store (whatever your stack's lightest option is), mutations through the server proxy.
  • Apply discount codes in your own cart drawer via cartDiscountCodesUpdate — Shopify validates and reprices, and you sidestep the payment provider's coupon UI entirely (ours never worked; a support ticket saga). Know Shopify's two allocation levels: order-wide codes land at cart level, product-targeted codes inside line prices — payable = subtotal − cart-level only.
  • Advertise only deliberately-public codes (we filter on a title prefix, PUBLIC …). And delete test discount codes immediately — our TEST1 (50% off!) sat live and redeemable for four days after its E2E test.
  • Free-shipping progress bar in the drawer, judged on the post-discount payable — the same number the shipping callback sees.

5) Payments — a provider-hosted checkout on a headless store

The single most scar-dense area. Note that a provider's Shopify plugin is theme-based and useless headless — you want their custom-platform / API integration, where you create the Shopify order after payment. Everything below generalises across providers; the exact field names are our provider's.

Setup traps

  • Every provider dashboard setting is mode-specific. Webhooks, callback URLs, feature toggles — sandbox/test and live are two entirely separate configs. This single fact produced at least three multi-hour debugging sessions. Configure both, verify both, and when something "isn't being called", check which mode you're in first.
  • Build-time-inlined env vars don't change on save. Changing the mode/key env var in your host's dashboard does nothing until you redeploy (the "public"-prefixed client-exposed vars in every major framework). The provider dashboard's test/live toggle changes what you see there — not which key your deployed site opens checkout with.
  • The publishable key is the only credential the browser sees; the secret never leaves the server. Default the integration to test mode so a fresh deploy is inert and can never charge by accident.
  • Know the field that opts an order into the enhanced checkout — without it, providers silently fall back to their basic payment UI (no address step, no delivery options) with no error anywhere (ours: line_items_total).
  • Send display metadata in line items — image URL (thumbnail-sized), product URL, variant description — or the order summary renders as a bare price row. Product URLs need a real reverse map if your storefront handles differ from Shopify's (ours do) — naive interpolation 404s on every product.
  • Merchant references/receipts: unique per order, alphanumeric only. One cart legitimately creates several provider orders (reopen checkout, edit cart, retry). We shipped references derived from the cart id alone and found two live orders of different amounts sharing one — and lookup-by-reference (newest wins) can then resolve the wrong amount → wrong shipping tier.
  • Provider SDKs claim window globals and survive client-side navigation. With client-side navigation the DOM swaps without a reload, so whichever script loaded first stays resident — if the provider has two SDKs (one-page checkout vs standard (ours did)), never test the global directly; use a loader that tags which script is resident and forces a real reload on mismatch.
  • Your order-creation endpoint is a card-testing target. Public, unauthenticated, creates payment intents — exactly what carding bots want for validating stolen cards in low-value bursts from rotating IPs. Rate-limit it, put WAF/bot rules in front of it, and turn on the payment provider's own card-testing protections before launch traffic makes you interesting. (Every hardening item in the forms section applies here with more at stake.)
  • Keep a working fallback checkout. The provider overlay should degrade to the platform's hosted checkout when keys are missing, the SDK fails to load, or order creation errors — a provider outage must not be a store outage. Test the fallback path on purpose; an untested fallback is a second outage.

Callback contracts (the field names are traps)

  • Never assume callback field semantics from their names. (Ours: the field literally named order_id carried our merchant reference, and the provider's own order id arrived in a different field with its id-prefix stripped.) Resolve provider-id first, then reference. This bit us twice — one callback first, its sibling months later, because the lesson was applied to one endpoint and not the other. When a contract lesson lands, sweep every endpoint that shares the contract.
  • Callback bodies arrive minimal, and references can travel in the query string — read both. Log the full request surface (raw body, query, all non-standard headers) on every callback from day one; logging only your parsed view shows order=- whether the field was absent or merely somewhere you didn't look.
  • Shopify's "free over a minimum" rate is one method definition with a price-range condition, which the Admin API renders as a same-named pair (unconditioned paid rate + conditioned free rate). Filter by subtotal and collapse same-named pairs to the cheapest applicable, or customers see the paid and free option side by side — and pick free.
  • The definitive "is the provider actually calling us" check is the user-agent. Real calls carry the provider's client UA and request-id headers (ours: a Go HTTP-client UA plus the provider's own request-id headers). Your own curl probes look identical in application logs. We spent rounds optimising a callback that — as the user-agent later proved — the provider had never called in live mode.
  • Don't probe production callbacks with invalid order references — providers may cache your serviceability answers per postal code, and your degraded fallback response can be served back to real shoppers. Probe previews, or use valid refs on fresh codes.

Webhook → order creation

  • Exactly one trigger event per payment mode. Providers fire multiple events at the same instant for one payment (ours: payment.captured alongside order.paid), and two racing past a read-then-write idempotency check → duplicate orders, double inventory decrement. Unsubscribe the redundant event; don't just ignore it in code.
  • Shopify's order search index is eventually consistent. A webhook re-delivery 3 seconds after the first saw zero results for the idempotency tag and created a second order. Idempotency needs three layers: in-flight set (same warm instance) → search-index query (old orders) → direct read of recent orders filtered in code (the index-lag window).
  • Provider orders often return no line items — stash the cart id in the provider order's metadata/notes at creation and refetch the cart in the webhook for authoritative lines.
  • Associate the customer by email (customer.toUpsert); omit phone — a number owned by another customer record fails the entire mutation. Email is the account-linking key: orders show in account history only when associated.
  • Expect API pedantry at the seams (ours: a JSON field that takes an object not a string; a zero-amount field where || was right and ?? was a bug; bundle parents that must be expanded to component variants before orderCreate accepts the line). Budget a real-payload test run for these.
  • Verify the webhook secret without a payment: self-sign an event your handler verifies-then-ignores and POST it at production. 200 = deployed secret matches your env; a bad signature must 401. What that can't prove: the secret stored in the provider dashboard — only a genuine delivery does.

6) Customer accounts (headless)

  • Customer Account API, confidential client, everything server-side: endpoints auto-discovered from .well-known, tokens sealed in an httpOnly+HMAC cookie, state+nonce validated. Register both the callback URI and the logout URI in Shopify — a live HTTPS domain is required.
  • Login is passwordless (email OTP or Google). Purchases silently find-or-create the customer record; login later "activates" it with orders already attached. The discrepancy risk is a customer checking out with a different email than their account — prefill the checkout overlay from the session to make matching the default.
  • Logout is three sessions and only two are yours. (1) Cross-origin logout redirects need a real page load — a framework reload attribute alone did NOT stop the client router's interception; force window.location.assign. (2) Shopify's session lingers briefly — set a short marker cookie so /account shows "signed out" instead of silently re-authenticating. (3) Google's session is not yours to end; Shopify supports prompt=none only — do not add prompt=login, we tried and reverted it.
  • Google Sign-In needs merchant credentials (your own Google Cloud OAuth client — not optional). shopify.com in the authorised domains is their documented requirement, not a mistake. Branding verification requires your homepage to describe the app in server-rendered text — brand name as text, not only a logo image (name-match fails on images). Expect redirect_uri_mismatch to be the actual blocker, not verification.
  • Addresses picked inside the payment overlay live in the provider's address book, invisible to Shopify. Offer them back for one-tap saving from order history ("From your orders") instead of pretending the gap doesn't exist.
  • Provider-side "customer accounts" / login products are usually gated on their Shopify app and theme pages — assume unsupported for headless until the provider confirms otherwise in writing, and note that even then the session they mint lives on the provider/Shopify domain, not yours.

7) Analytics & conversion tracking

  • Instrument the whole funnel, not just purchase: product view → add-to-cart → begin-checkout → the provider overlay's own journey events (payment initiated / failed / checkout abandoned, with time-in-checkout) → purchase. Analytics platforms only derive abandonment as begin minus purchase; the overlay's event hook carries the why and the when — forward those events into your analytics from day one.
  • Dedupe the purchase event twice: browser storage and transaction id (the provider order id). Confirmation pages get reloaded, revisited and shared; a purchase that fires per pageview quietly corrupts every number downstream.
  • Gate the confirmation page on a server-verified payment signature before rendering order details or firing purchase — the ids arrive as redirect URL params, and URL params are forgeable.
  • Client-side pixels are launch-sufficient; server-side conversions (fired from the payment webhook) are a planned post-launch upgrade — reserve the env keys, don't block launch on it.
  • Tag test-mode orders as tests everywhere (order flag, analytics exclusion) so rehearsals never pollute revenue reporting.

8) Domain architecture, SEO & Google Merchant Center

Learned via a misrepresentation suspension. The root cause was architectural, not content: GMC's claim + feed pointed at the Shopify-theme subdomain while the real store was the headless site — two storefronts, one catalog.

  • One canonical host (apex vs www — pick one, 308 the other), a sitemap that filters out utility/noindex routes, and a robots.txt you actually control.
  • Server-render product structured data (JSON-LD) on PDPs from the same catalog the page renders — schema that can't drift from the visible price, for the same reason the feed is built from it (below).
  • Claim the root domain via a meta tag your layout renders on every page.
  • Self-generate the product feed (/feed.xml) from the same build-time catalog the PDPs use — price/availability mismatches become impossible by construction. Register it as a scheduled-fetch primary feed; keep the website auto-crawl source off.
  • Never (re)link the Shopify ↔ GMC integration — it builds links from Shopify's primary domain (the checkout subdomain, unfixable for headless) and can overwrite your claim, feed, and shipping settings. Uninstall the Google & YouTube / Facebook / Pinterest sales channels; feed Meta and Pinterest from your own domain later if needed.
  • Lock the checkout subdomain down: products unpublished from Online Store, a noindex+redirect theme as the live theme, robots.txt without a Sitemap: line (crawling stays open so the noindex is seen). Its only jobs: checkout processing and order-status pages.
  • GMC shipping settings, the policy page, and the delivery profile are three manually-synced copies of one fact — write that down where the threshold is changed.
  • Reinstatement reviews are effectively one shot (failed appeals stack cooldowns) — appeal once, itemized, after everything is verifiably fixed.

9) Public write endpoints (forms)

  • Any public unauthenticated POST that writes gets, from day one: honeypot (answer success — a failure response teaches the bot which field to skip), server-side validation as the enforcement (client required is a courtesy), per-IP rate limit checked before body parsing, and an absolute record ceiling — metaobjects are capped per shop, so an unbounded endpoint can exhaust a store-wide resource.
  • The rate limit shares an Admin API budget with order creation (same app, same shop). A flood competes with the call that turns payments into orders — that's the reason to bound it, not the junk records.
  • A capacity guard must cost zero upstream calls once tripped (cache the count) — a guard that spends the budget it protects is worse than none.
  • Real edge protection (WAF rate-limit rules, bot detection) belongs in front of the function — most hosts don't bill WAF-blocked traffic; your in-code guard runs after invocation. Stage rules log → review → enforce.
  • One record per applicant (upsert by natural key); re-submission updates rather than duplicates, and must never reset merchant-owned triage fields (read first, write status only on create).

10) Hosting ops

  • Set the function region to your market. The host default is usually US-East; ours ran every cart call and payment callback across an ocean for weeks before we noticed. And don't measure the fix from a laptop — client latency swamps it; read execution duration in the function logs.
  • Prefix every log line with a greppable tag and log decisions (what was offered, what was resolved, what was ignored) — inbound-only logs can't distinguish "degraded" from "fine". Make degraded paths loud.
  • All third-party callbacks answer GET with 200 — URL validators and curious browsers otherwise hit your error renderer. One dashboard validator rejected multi-segment paths entirely; we ship a single-segment alias route for exactly one picky form field.
  • Timeouts + fail-strategy for every third-party call in the checkout path. Decide fail-open vs fail-closed per call on consequences: rates fail closed (can't price = can't charge correctly), courier serviceability fails open (a false "we don't deliver" loses the sale; a false "we do" costs one refund). Run independent lookups concurrently — checkout blocks on you, so cost max(), not sum().
  • Decide what wakes a human — logs are not alerts. Form submissions sit unseen in an admin screen, webhook failures exist only in host logs, and degraded fallback paths are silent precisely when they matter most. For every best-effort path, write down where its failure surfaces and who notices — "nothing notifies you" should be a documented decision, never a discovery.
  • .env.example is documentation. Every variable annotated with which dashboard screen issues it, the exact scopes needed, and whether it's build-time-inlined (i.e. needs a redeploy to change). It's the difference between a handover and an archaeology dig.
  • Know your build tooling's scanning quirks (ours: a utility-CSS engine that only emits classes found literally in source — runtime-injected class names silently render as nothing — and whose file scanner, walking a 27k-file design-reference folder, took down the dev server until the folder was excluded).

11) Email

  • Sender = a domain address (support@…), not a personal Gmail — ours shipped with one until a pre-launch audit caught it.
  • Authenticate the domain before the first order: SPF (include:shops.shopify.com), Shopify's DKIM CNAMEs, DMARC. Unauthenticated = "via shopifyemail.com" + spam folder, on the one email (order confirmation) that is your post-purchase experience.
  • Know which emails cannot exist in your architecture: with an on-site payment overlay there is no Shopify checkout object, so Shopify abandoned-checkout emails never fire. Recovery has to come from the payment provider's webhook. Don't wait for emails that can't arrive.

12) Launch — one real order proves the chain

  • Remove the coming-soon gate deliberately, and know your host's rewrite semantics — ours ran after the filesystem check, so it never applied to prerendered pages and the "gate" was partly fictional the whole time.
  • The first-order test. Place one real (or provider-test-mode) order end to end, then verify in sequence: webhook delivered and answered 200 — the only real proof the secret stored in the provider dashboard matches yours; Shopify order created with correct lines, address and total; customer associated (the order appears in their account history); inventory decremented exactly once; confirmation email from your domain, not in spam. Then refund it.
  • Until that has run, everything upstream is design intent — treat "we tested each piece" and "the chain has run once" as different claims, because they are.
  • Keep the first week's dashboards open: provider webhook delivery history, function logs filtered to your log tags, and the provider's event feed. The failure modes that survive testing are the quiet ones.

13) Verification discipline (the meta-lessons)

  • Real payloads beat documented payloads. One callback body arrived empty with the data in the query string; "no order id despite what the docs show" is a direct quote from our code comments. When docs are thin, read the provider's own open-source plugin — it has to parse whatever they actually send (that's how we recovered our shipping-callback contract).
  • E2E-test the webhook with self-signed real payloads before going live — it's what exposed our duplicate-order race. Delete the test orders and restore inventory after.
  • Real-browser screenshots at a mobile and a desktop viewport (ours: 390px and 1280px, via Playwright) for anything visual — a browser extension can't emulate viewport. Blank voids in full-page screenshots are a lazy-load capture artifact, not a bug; measure the DOM instead.
  • Check the user-agent before debugging any "callback not working" report — distinguish your own probes from the provider's real traffic first. Hours live in this checkbox.
  • Keep a mode-config parity list: every setting that exists twice (test/live) — webhooks, callback URLs, plans, keys — and verify both sides whenever either changes.
  • When a fix ships, re-test against the deployed code, and when a claim turns out wrong, write the correction into the docs where the claim was made — stale confident claims cost more than gaps.

14) Market specifics — find your market's equivalents

Every market has a version of each of these; the checklist item is knowing yours before checkout is designed, because each has code consequences.

  • Payment-method norms. Prepaid vs cash-on-delivery is a business decision with consequences everywhere: payment methods offered, which webhook events exist, order financial status (PAID vs PENDING), and copy on policy/terms pages. Retiring COD (ours) meant a callback flag, a dropped webhook event, and a copy sweep.
  • Tax presentation norms. Some markets legally expect tax-inclusive consumer pricing; others expect tax added at checkout. Make Shopify's "all prices include tax" setting match your market's reality or the fallback checkout and order records disagree with what customers actually pay.
  • Address shape. Providers send full region names, Shopify wants ISO subdivision codes — you'll need an explicit mapping table. Postal-code formats and validation differ per market.
  • Privacy & consent regime. Many jurisdictions now require opt-in consent before analytics and marketing pixels fire — with fines that have real commas in them — while others allow opt-out or nothing. If your market (or a market you ship to) requires it, a consent manager that actually blocks the scripts until consent is not optional, and your banner must match what the tags really do.
  • Accessibility as law, not polish. Several jurisdictions have made WCAG 2.1 AA a legal requirement for e-commerce, with active enforcement. Even where it isn't law, the design-system items above (contrast tokens, focus states, reduced motion, DOM order) are the cheap time to comply — retrofitting is the expensive time.
  • Courier serviceability granularity. Shopify delivery profiles only know countries; couriers serve postal codes. Without a courier-level check, every address reads deliverable and unreachable customers can pay (refund + a bad first order). Wire the courier's serviceability API behind your own shipping callback — fee stays yours (your pricing policy), only deliverable yes/no comes from the courier; never quote the courier's internal rate card to a customer.

Extensions — add these when the business needs them, not before

None of these belong in the base build. Each is a real capability with a real Shopify path, and the discipline is the same for all of them: build behind demand, not ahead of it. We learned that one directly — see the first entry.

  • Recurring payments / subscriptions. We built and end-to-end-tested a full subscription system — then switched signups off, because demand hadn't arrived and a live recurring-billing surface you don't need is pure risk. The base build now just registers interest (one analytics event on a coming-soon card) and that signal decides when to flip it on. Traps we hit while building it, banked for when you need them: provider subscriptions are a separate product from the one-page checkout (own SDK, own webhook lifecycle — keep the two scripts from ever sharing a page); creating the subscription before opening its checkout leaks abandoned created attempts — filter them out of listings and reuse them on retry; pause/resume are state-restricted (pausing a mandate that was never authorised can cancel it outright); and plans cannot reprice — a price change means a new plan, with existing subscribers grandfathered.
  • International & multi-currency. Shopify Markets covers pricing, currencies and duties — but a headless frontend must consume it deliberately (contextual pricing in every Storefront query, per-market URLs, translated content). Add it when a second market has real demand; every catalog query you write before then should at least not preclude the buyer-context parameter.
  • B2B / wholesale. A separate Shopify surface (and plan tier) with its own pricing, catalogs and checkout rules. A different project sharing your codebase, not a feature flag.
  • A headless CMS. Deliberately a fork, not a default — see the content-home decision in section 0. Add one — we've reached for Sanity.io — when the client's editors need self-serve publishing; a one-or-two-person team is usually better served by content-as-code. Migrating content twice is the cost of deciding late.
  • Native apps / additional surfaces. The one-catalog-many-surfaces promise is real (our product feed is a small proof), but each new surface is its own consumer of the same APIs with its own lifecycle. Justify each by audience, not by architecture.
  • Server-side conversion tracking. Already staged in the analytics section: reserve the env keys at launch, wire it when ad spend makes the signal worth the plumbing.

What this checklist doesn't cover (yet)

Silence elsewhere in this document means "we did it and it held". Here it means "we haven't earned the lessons" — and unlike the extensions above, these are not optional:

  • Post-launch order operations — refunds, returns, partial fulfilment, disputes/chargebacks. Every store eventually runs these; we haven't yet. Treat the omission as ignorance, not simplicity.
  • Enterprise integration surface — ERP, PIM, IMS, OMS. This checklist is a small-catalog D2C build; if headless is your integration layer for a back-office stack, you have a different project with different failure modes, and this document only covers the storefront half of it.
  • Automated test suites and CI. Our verification was disciplined but manual-plus-scripted: typecheck on every change, real-browser screenshots, self-signed webhook payloads, live-endpoint probes. There is no unit-test suite and no CI gate — and this codebase computes money (unit-price rounding, discount splitting across lines, shipping-threshold tiers), which is exactly what deserves one. The verification-discipline section is what we did instead; treat it as a floor, not a substitute.

All of it earned the hard way — a commit history is the order in which it actually hurt. Take the list, skip the tuition.

Related reading