Wayfinder: chart the Shopify headless commerce map

The route from "mozimo.in displays Shopify products" to "mozimo.in sells them",
as a plan with a phase spine and gates, plus the open decision tickets that
still shape it and the evidence they rest on.

- .scratch/shopify-headless-commerce/: plan.md (phases and gates), map.md (the
  wayfinder map), 10 open decision tickets
- CONTEXT.md: Storefront / Theme store / Cart / Parity vocabulary
- docs/adr/0001-0003: Checkout stays Shopify-hosted, one Variant per product,
  the checkout domain stays shop.mozimo.in
- docs/research/ and docs/shopify-catalog-inventory.md: verified store facts,
  the Cart API probe, and the catalogue inventory

Raw Shopify doc scrapes under .scratch/shopify-research/ are ignored: they are
working material, not record.
This commit is contained in:
tanshu committed 2026-10-14 09:00:42 +05:30
1 parent bb0c78fec5
commit de0553d0d4
21 files changed
+3483

No files matched your search

@@ -0,0 +1,44 @@
# Seed the dev store from the live catalogue
`Type:` task (HITL)
`Status:` open
Part of [the map](../map.md). Empty the frontier from the top.
## Question
The dev store `staging-iw0gviik.myshopify.com` is empty. How does it come to hold the live catalogue faithfully, so that Phase 1 Cart work is built against realistic data?
The manual CSV route was chosen for this. Verification has now shown exactly where it holds and where it **cannot**:
### ✅ Products, and product metafields with preparation
- Export: **Products → Export** → scope → *Plain CSV* or *CSV for Excel/Numbers* → **Export products**. One file. Note that an **All products export is emailed**, not downloaded.
- Import: **Products → Import** → add file → *Overwrite products with matching handles* → **Import products**.
- Product metafields **do** ride in the CSV as their own columns, named `<Name> (product.metafields.<namespace>.<key>)`. `custom.ingredients` as multi-line text and `custom.energy_kcal` as a number are both documented-supported types.
- **Precondition:** the metafield **definitions must already exist in the destination store** (Settings → Custom data → Products). Shopify's docs only promise columns for *defined* metafields. What happens to an undefined metafield's value is undocumented — so create the definitions before importing, not after.
- Hazards: with *Overwrite* on, a **blank metafield cell overwrites the destination value as blank**. Images are URLs only and Shopify re-hosts them at import, so production's products and images must stay publicly reachable until the import finishes. There is **no dry run**, imports cannot be cancelled, there is no import history, and the file cap is 15 MB.
### ❌ Collection membership — impossible by CSV
The `Collection` column is **never exported**, and on import it is the only column you may add — and it admits **one collection per product**. Most of this catalogue belongs to three or four collections each: `mozimo-carnival-celebration-m` is in `best-sellers`, `gift-collection`, `diwali` and `hampers-boxes` at once. Sixteen collections and their multi-membership therefore need a **second mechanism**.
This part re-opens the seeding decision, because the chosen route cannot finish the job alone.
### ⚠️ Variant option shape — read the real export
Shopify's docs say a one-option product should carry `Default Title` in `Option1 value`, while the **official sample CSV represents its single-option product as `Option1 name = Title`, `Option1 value = Premium`** — which is exactly this catalogue's stored shape. The docs and their own sample disagree, and the documented hazards around *changing* option values (variants are deleted and recreated, so IDs change) make this worth confirming rather than assuming. Exporting from production settles it, and that export is needed anyway.
## To resolve
1. Export the full catalogue from production and record what `Option1 name` / `Option1 value` actually contain.
2. Create the metafield definitions in the dev store before importing.
3. Run a **small-batch import with *Overwrite* off** to surface per-line errors cheaply, then the full set.
4. Decide the second mechanism for collection membership — admin bulk-editor passes, or a small Admin API script using the credentials the repo already mints. Record which was chosen and why.
5. Record what actually landed: product count, collection count and membership, metafield coverage, anything rebuilt by hand.
Facts later tickets depend on: which products exist in the dev store, whether their Variant GIDs differ from production's (they will), and whether the metafield values survived.
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,19 @@
# Cart persistence and SSR hydration
`Type:` grilling (HITL)
`Status:` open
`Blocked by:` 01
Part of [the map](../map.md).
## Question
How is a Cart identified and carried across requests? Shopify addresses a Cart by ID, and this Storefront renders on the server, so the mechanism has to survive both.
Decide: where the Cart ID lives (an httpOnly cookie the server sets, client storage, or both), what its attributes are, and how the client hydrates without a flash of an empty Cart.
Decide the failure paths too, because both are reachable: a Cart expires after 30 days, and Shopify deletes it on checkout completion — so a returning buyer can arrive holding a cookie that points at nothing. What happens then — silent re-create, cleared Cart, or an error?
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,22 @@
# Degraded-mode experience
`Type:` grilling (HITL)
`Status:` open
Part of [the map](../map.md).
## Question
When Shopify is unreachable, what does a buyer actually get?
The Storefront currently falls back to the Editorial catalog, which is display-only and whose prices can drift from commerce truth. We decided that stale **real** data beats fabricated data, with buying disabled. This ticket settles what that means concretely:
- When is degraded mode entered, and how is it detected — a failed Cart call, a failed product fetch, or both?
- What does a buyer see? Does a notice appear, what does it say, and is it brand-appropriate for a luxury storefront?
- Is stale data served transparently, or is the page visibly marked as out of date?
- What exactly is disabled — the add-to-cart control, the Cart itself, or the route to Checkout?
- What happens to an existing Cart mid-session when the API drops?
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,25 @@
# Prototype the Cart surfaces
`Type:` prototype (HITL)
`Status:` open
`Blocked by:` 02
Part of [the map](../map.md).
## Question
What do the mini-cart drawer and the `/cart` page actually look like, and what do they do?
Raise the fidelity of the discussion cheaply: a rough, concrete prototype of both surfaces is worth more than further prose. React to it and settle:
- What the drawer shows on add-to-cart — the added line only, or the whole Cart?
- Line-item controls: quantity, remove, and how the gifting note attaches.
- Where subtotal, discount entry and any shipping-threshold messaging sit.
- How the drawer and the full page relate — does one open the other?
- The transition to Checkout: what the button says, and how the handoff to `shop.mozimo.in` is framed so it doesn't read as leaving the brand.
The design language is editorial and restrained; the Cart is the first transactional surface in an otherwise editorial site, which is the tension worth prototyping.
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,22 @@
# The gifting model
`Type:` grilling (HITL)
`Status:` open
Part of [the map](../map.md).
## Question
What is the gifting model on the Storefront?
A gift note as a Cart attribute is in Phase 1, and wrap and delivery options are Phase 3. Settle:
- Which of note, wrap and scheduled delivery actually apply to how this brand sells.
- Where each is entered — product page, drawer, or `/cart`.
- Whether it is per-Cart or per-line. A hamper and a bar in one order may need different messages.
- Whether wrap or scheduled delivery carry a charge, and how that appears in the Cart.
- What the Cart records versus what Checkout renders. Cart attributes flow through to the order; scheduled delivery in particular tends to need Shopify-side configuration, so establish what is even possible before promising it.
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,20 @@
# Discount configuration check
`Type:` task
`Status:` open
Part of [the map](../map.md).
## Question
Does this store run any discounts?
The Storefront API cannot answer this: `discountCodes` and `discountNodes` are not fields on `QueryRoot` at 2026-10, and discount data is only reachable per live Cart. Whether a discount exists is therefore unverifiable from code, and the parity checklist cannot be written without knowing.
Resolve by checking Shopify admin directly: are there active discount codes, automatic discounts, or abandoned-checkout style offers? Do any apply by default at checkout?
Record what exists — codes, types, conditions, expiry — and note which of them a headless Cart would need to reproduce (`cartDiscountCodesUpdate` applies a code, but automatic discounts are Shopify-side).
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,28 @@
# The Parity checklist
`Type:` grilling (HITL)
`Status:` open
`Blocked by:` 05, 06
Part of [the map](../map.md).
## Question
What exactly must the Storefront be able to do before the Theme store's pages are retired?
Phase 5's gate is "the parity checklist is met", which is circular until the checklist exists. Enumerate it and settle what is genuinely required versus merely nice:
- Guest checkout, and confirmation that no account is ever needed to buy.
- Discount codes, per whatever the configuration check found.
- Gifting, per the gifting model.
- Shipping and delivery options as a buyer experiences them. India-only is settled; what is shown about it is not.
- Order confirmation and transactional email, which Shopify sends but which currently carries the Theme store's branding.
- Order history, which depends on Phase 4 — so decide whether Phase 4 gates this or merely improves it.
- Search, and whether the existing predictive search is sufficient.
- Anything else a buyer can currently do on `shop.mozimo.in` that mozimo.in cannot.
For each item, mark whether it gates retirement or is deferred. Anything that gates retirement has to be finished before Phase 5, so this checklist sets the real scope of Phase 3.
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,23 @@
# Audit what points at the Theme store
`Type:` task (HITL)
`Status:` open
Part of [the map](../map.md).
## Question
What currently sends traffic to `shop.mozimo.in`?
This cannot be answered from the codebase alone, and finding a live campaign pointed at a Theme store URL two days before cutover is the classic way this migration goes wrong. Inventory:
- **In the repo** — every hardcoded `shop.mozimo.in` reference, including the header's account and Bag icons, and any product URL built from the Shopify domain. (The audit should cover this, but the repo half is discoverable now.)
- **Outside the repo** — paid ad campaigns and their landing URLs, Google and Meta shopping feeds, Instagram and Facebook shop, marketplace listings, email templates, and printed packaging or QR codes.
For each: what it points at, whether a mozimo.in equivalent exists, and whether it needs updating before or after cutover.
This gates Phase 5 and sets how much redirect coverage is actually required.
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,22 @@
# Catalogue content gaps
`Type:` grilling (HITL)
`Status:` open
Part of [the map](../map.md).
## Question
Once the full 61-product catalogue is surfaced, most product pages will render without the content the design assumes: 41 of 61 products have no ingredients and no nutrition metafields, and `custom.tagline` is populated on exactly one product.
Settle how that is handled:
- Does the Storefront absorb it with conditional rendering, and what fills the space left behind? A product page with a missing panel can look broken rather than sparse.
- Or is the data populated in Shopify first, which is content work rather than engineering work?
- Or do the products without content stay unsurfaced until content exists, which contradicts surfacing the full catalogue but keeps every page complete?
Also settle the dead compatibility path this exposes: the entire `facts.*` metafield namespace is empty, so the `facts.nutrition` / `facts.ingredients` / `facts.storage` branch in `shapeProduct` can never fire and should be removed or justified.
## Answer
<!-- filled on resolution -->
@@ -0,0 +1,23 @@
# Confirm the customer accounts version
`Type:` task
`Status:` open
Part of [the map](../map.md).
## Question
Is this store on new customer accounts or legacy (classic) customer accounts?
Everything points to **new**: the admin panel exposes **Self-serve returns** and **Store credit** (✔ for new accounts, ✘ for legacy in Shopify's own comparison table), and the account URL is the `shopify.com/<id>/account` form that only the new system uses.
But that URL is **not conclusive**. Shopify's upgrade guide populates the new URL *before* cutover, so a store mid-migration can show it while still serving legacy account pages. Legacy accounts were deprecated on 2026-02-26 and the **sunset date is still unannounced** as of October 2026 — so if this store is genuinely still on legacy, Phase 4 stops being a comfortable late phase and has to move ahead of the sunset.
There are two clean discriminators, either of which settles it:
- The **Upgrade banner** on Settings → Customer accounts.
- Admin GraphQL `Shop.customerAccountsV2.customerAccountsVersion`, which returns `CLASSIC` or `NEW_CUSTOMER_ACCOUNTS`.
## Answer
<!-- filled on resolution -->
+43
View File
@@ -0,0 +1,43 @@
# Map — Shopify Headless Commerce
The wayfinder map for moving mozimo.in from *displaying* Shopify products to *selling* them.
The destination, the phase spine and its gates live in [`plan.md`](./plan.md). The evidence behind them lives in [`docs/research/`](../../docs/research/) and [`docs/shopify-catalog-inventory.md`](../../docs/shopify-catalog-inventory.md).
## Destination
A Storefront on which a buyer can assemble a Cart, review it, and complete a purchase — with `shop.mozimo.in` reduced to the Checkout domain and its storefront pages retired.
## Notes
**Vocabulary:** [`CONTEXT.md`](../../CONTEXT.md). Use **Storefront**, **Theme store**, **Cart** and **Checkout** as defined there; the Cart/Bag and Storefront/Theme-store distinctions matter in every ticket.
**Skills for every session:** `grilling` and `domain-modeling`. For Shopify API shapes, the official skill bundle is installed at `.agents/skills/shopify-storefront-graphql/` but is **not registered** with the harness — `skill` will not load it. Its `scripts/search_docs.js` works when run directly and is the fastest way to check a mutation's real field names; its `assets/` schema snapshot is `2026-01`.
**Decisions settled before this map was charted** — the destination, the spine and gates, the verified store constraints, and what is out of scope — are recorded in [`plan.md`](./plan.md), not here. This map tracks what is still open.
**Shopify docs method:** appending `.md` to a `shopify.dev` URL returns the untruncated raw Markdown, and the same trick on `help.shopify.com` — with an `/en/` locale prefix — bypasses its 403 bot challenge. Use those rather than reading HTML.
**Standing preference:** plan, don't build. Tickets resolve decisions; implementation is a later, separately scoped effort per phase.
## Decisions so far
<!-- one line per closed ticket, enough to judge relevance, then the link -->
## Not yet specified
- Whether the Storefront needs a returns or refunds surface of its own. The store's self-serve returns toggle is off, so this may stay a policy page.
- What transactional email looks like once buying moves: Shopify sends order confirmations, but they currently carry the Theme store's branding.
- Where the 46 products absent from the Editorial catalog should sit in the Storefront's merchandising, once the full catalogue is surfaced.
- Whether the 16 live collections need a new URL shape on the Storefront, and what that costs in SEO.
- Cart edge cases beyond the documented limits: what a buyer sees when a Cart expires mid-session, and how quantity ceilings are communicated.
- Post-purchase order tracking on mozimo.in, which may belong with Phase 4.
## Out of scope
- **Fully on-domain checkout.** Plus-only; revisit only via a plan upgrade. See [ADR-0001](../../docs/adr/0001-checkout-stays-shopify-hosted.md).
- **Multi-currency and Shopify Markets.** One currency (INR), one country (IN).
- **Subscriptions and selling plans.** No product requires one.
- **Gift cards.** None exist in the store, and none are queryable on the Storefront API.
- **Inventory and stock display.** Explicitly not tracked and not wanted; sold-out is not a reachable state.
- **Customer account migration.** The store almost certainly uses new customer accounts — self-serve returns and store credit are both present, and the account URL is on `shopify.com`. A single admin check confirms it, and either way no migration is in scope.
+138
View File
@@ -0,0 +1,138 @@
# Shopify Headless Commerce — Plan
The phased route from "mozimo.in displays products from Shopify" to "mozimo.in sells
them", and the retirement of the theme store's pages.
This plan holds the spine and the gates. The open decisions inside each phase are
held as wayfinder tickets on `map.md` in this directory.
## Destination
A Storefront on which a buyer can assemble a Cart, review it, and complete a purchase —
with `shop.mozimo.in` reduced to the Checkout domain and its storefront pages retired.
## Settled constraints
Facts, verified against the live store and Shopify's documentation in October 2026.
Sources: [`docs/research/shopify-headless-commerce-2026-10.md`](../../docs/research/shopify-headless-commerce-2026-10.md),
[`docs/research/shopify-cart-probe-2026-10.md`](../../docs/research/shopify-cart-probe-2026-10.md),
[`docs/shopify-catalog-inventory.md`](../../docs/shopify-catalog-inventory.md).
| Constraint | Value |
| --- | --- |
| Production plan | **Grow** — checkout UI extensions and checkout branding are Plus-only, so custom checkout is unavailable by plan |
| Storefront API version | Pinned in code as `2025-01`, a retired version currently served as `2025-10`. Target is **2026-10** |
| Checkout | Shopify-hosted, reached via the Cart's `checkoutUrl`. The Storefront never renders it |
| Checkout host | **`shop.mozimo.in`** — store-level config, not the host the API is called on |
| Catalogue | **61 products**, each with **exactly one Variant**; sizes are separate products |
| Currency / market | Single currency **INR**, single country **IN**. No Markets complexity |
| Shipping | **India only.** `shipsToCountries` currently lists 29 destinations and disagrees with the published policy |
| Inventory | **Not tracked, and not wanted.** `availableForSale` is `true` for all 61 products, permanently |
| Gift cards | **None exist**, and none are queryable on the Storefront API |
| Customer accounts | **New** customer accounts — strongly indicated by the admin panel, not certain. The legacy sunset is still unannounced, so verify rather than assume |
| Carts | Not shared with the theme store, and cannot be — the Ajax API is theme-only |
| Catalogue seeding | The product CSV round-trips products and product **metafields** (definitions must exist first), but **cannot** carry collection membership — the `Collection` column is import-only and permits one collection per product |
| Cart limits | 500 lines per cart, 250 lines per request, expire after 30 days, deleted on checkout completion |
## Phase spine
Each phase ships independently. The gate is what makes it a phase rather than a task list.
### Phase 0 — Foundations
Prerequisites for anything that writes.
- Pin the Storefront API to `2026-10` and log `X-Shopify-API-Version` so drift is visible
instead of silent. Remove the stale `2025-01` comment about `predictiveSearch`.
- Carry the Variant GID and per-variant price in `ShopProduct` — an add-to-cart needs a
merchandise ID and there is none today.
- Send `Shopify-Storefront-Buyer-IP` on server-side calls.
- Standardise the credential path on the minted OAuth token.
- Shopify admin data fixes: shipping zones down to India only; populate `productType`
and weight tags so the collection filter chips and card weights stop reading nothing.
- Degraded mode: prefer stale cached real data over the Editorial catalog, and disable
buying when the Cart API is unreachable.
- Fix the broken `/products/rakhi-bark-box-200gm` link.
**Gate:** product and collection pages render as they do today, every product exposes a
Variant GID, and the served API version is `2026-10`.
### Phase 1 — Cart
- Shopify Cart API: create, add, update, remove, query.
- The Cart ID persisted in an httpOnly cookie, hydrated safely under SSR.
- Mini-cart drawer and a `/cart` page, with editable lines.
- Add-to-cart on the product page.
- Gift note captured as a Cart attribute.
- Analytics: `add_to_cart`, `view_cart`, `remove_from_cart`.
**Gate:** a guest can add, edit and remove; the subtotal is correct; the Cart survives a
reload and a browser restart.
### Phase 2 — Checkout handoff
- Redirect to the Cart's `checkoutUrl`.
- Fire `begin_checkout` with the real Cart.
- Handle price change and Cart expiry at handoff. Sold-out is not a reachable state.
- Verify an India address completes and a non-India address is refused.
- Complete at least one test-mode order through the Bogus Gateway.
**Gate:** a genuine test-mode order completes end-to-end from mozimo.in.
### Phase 3 — Commercial parity
- Surface the full 61-product catalogue.
- Discount code entry in the Cart. Whether any discount exists is unverifiable from the
Storefront API, so this is built against configuration, not against observed data.
- Gifting: wrap and delivery options beyond the Phase 1 gift note.
- Quick-add from collection grids.
- Close the content gaps that make product pages look broken at scale: 41 of 61 products
have no ingredients or nutrition, and `custom.tagline` is populated on exactly one.
- Rebuild collection pages against the 16 live collections.
**Gate:** the parity checklist is met and the Theme store adds nothing commercially.
### Phase 4 — Accounts
- Customer Account API: hosted login, order history, saved addresses.
- Retires the `shop.mozimo.in/account` handoff.
**Gate:** a customer can sign in and see their orders without leaving mozimo.in.
### Phase 5 — Retire the theme storefront
- `shop.mozimo.in` **stays attached to Shopify as the Checkout domain**. Checkout is
served from it, so the domain cannot be retired — only the Theme store's pages can.
- Redirect the theme's **storefront paths** only, leaving Cart and Checkout paths intact.
See [ADR-0003](../../docs/adr/0003-checkout-domain-stays-shop-mozimo-in.md).
- Channel audit: paid ads, shopping feeds, marketplace listings, printed QR codes.
- SEO: sitemap, canonicals, redirect coverage.
**Gate:** the theme's pages are retired, Checkout still resolves, and no channel depends
on the theme storefront.
## Out of scope
Work consciously ruled beyond this destination.
- **Fully on-domain checkout.** Plus-only. Revisit only via a plan upgrade, which would
be a fresh effort, not a resumption of this one.
- **Multi-currency and Shopify Markets.** One currency, one country.
- **Subscriptions and selling plans.** No product requires one.
- **Gift cards.** None exist in the store.
- **Inventory and stock display.** Explicitly not tracked and not wanted.
- **Customer account migration.** The store already uses new customer accounts, so there
is no legacy sunset to outrun.
## Open decisions
Held as tickets on `map.md` in this directory, wired with blocking edges.
- How the dev store is seeded from the live catalogue.
- Cart persistence and hydration design under SSR.
- What degraded mode looks like to a buyer.
- The gifting model: note, wrap, delivery date.
- Where the Cart lives in the UI, and what the mini-cart drawer must show.
- The parity checklist that gates Phase 5.
- What currently points at the Theme store.
- Whether the Storefront should absorb the catalogue's content gaps.