Files
mozimo.in/.scratch/shopify-headless-commerce/issues/01-seed-dev-store.md
T
tanshu 1bc1d5f753 Seed the dev store's collections from a production snapshot
The AFK half of the wayfinder ticket "Seed the dev store from the live
catalogue". Products, metafields and images still come from the CSV export —
the CSV re-hosts images for free — but collection membership cannot: the
Collection column is import-only, takes one collection per product, and this
catalogue runs 1.6 memberships per product across 16 collections.

- scripts/seed-dev-store.mjs (`npm run seed:dev-store`): creates each missing
  collection with its members in one call and publishes it. Uses
  CollectionCreateInput.sources because collectionAddProducts is deprecated at
  2026-10, and publishes because collectionCreate leaves a collection
  unpublished — invisible to the Storefront API the dev store exists to serve.
  Idempotent, never deletes, reports handles it cannot find (which is how a
  partial CSV import shows up), and is a dry run unless given --apply.
- assets/dev-store-seed.json: the baseline the seed is checked against and the
  membership map it consumes — 61 products with option values, weights and
  product types, 16 collections with 100 memberships, and the 10 metafield
  definitions that must exist before the import.
- The ticket now records what the probes settled: the option shape is readable
  from the Storefront API (all 61 use Option1 name=Title; 22 are `Default
  Title`, 39 carry real values, so the import must not normalise), and the
  repo's production credentials are Storefront-scoped only — `products`,
  `collections` and `productsCount` are Access denied without read_products —
  so the ticket's "Admin API script using the credentials the repo already
  mints" was never available. A dev-store custom app is required instead.
2026-10-16 10:11:32 +05:30

11 KiB
Raw Blame History

Seed the dev store from the live catalogue

Type: task (HITL) Status: claimed

Part of the map. 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

Comments

AFK half worked — and the seeding decision re-opens further

Progress against To resolve: #1 is settled without an export; #4's stated option is dead and needs a decision the agent cannot make alone.

1. Variant option shape — settled (To resolve #1) ✅

The Storefront API exposes Product.options, so the export is not needed to read the shape. All 61 products carry one option named Title. The values are not uniform:

Option1 value (= Variant title) products
Default Title 22
100gm 17
200gm 11
60gm 5
1 3
250gm, 150gm, 12pc 1 each

So the sample CSV's shape (name Title, a real value) covers 39 of 61 and the docs' Default Title covers 22. The import must preserve each product's own value: changing an option value deletes and recreates the Variant, and the resulting Variant GIDs are what Phase 1 carts. Do not normalise. (ADR-0002's claim that weight lives in the Variant title holds for 39 products, not all 61; 22 are Default Title.)

2. The repo's credentials cannot drive an Admin API seed (To resolve #4) ❌

The production app's granted scopes are Storefront-only:

unauthenticated_read_product_listings   unauthenticated_read_product_inventory
unauthenticated_read_product_tags       unauthenticated_read_checkouts
unauthenticated_write_checkouts

products, productsCount and collections all return Access denied … Required access: 'read_products' access scope. So "a small Admin API script using the credentials the repo already mints" is not available as written. Any Admin API mechanism needs a custom app installed in the destination store with write_products.

Two consequences beyond this ticket:

  • Production's collection structure (manual vs smart, and any smart rules) is also unreadable with today's scopes, so a collection rebuild can only reproduce today's membership as manual collections — a real fidelity gap, not a bug.
  • The Phase 0 admin data fixes (61 × productType, 41 × weight) could be scripted rather than hand-edited if an Admin-scoped app is installed on production.

3. Metafield definitions to create first (To resolve #2) — 10 keys

The Storefront API cannot enumerate metafields (metafields demands explicit identifiers), so this comes from docs/shopify-catalog-inventory.md §6, measured with an explicit identifier list. These are the populated keys, i.e. the columns the CSV will actually carry:

definition type populated
custom.ingredients multi_line_text_field 20
custom.energy_kcal, custom.protein_g, custom.total_fat_g, custom.saturated_fat_g, custom.carbohydrate_g, custom.sodium_mg number_decimal 20 each
custom.sugar_g number_decimal 18
custom.added_sugar_g number_decimal 17
custom.tagline single_line_text_field 1

The facts.* namespace is empty catalogue-wide, so no facts.* definitions are needed for a faithful seed (ticket 09 decides whether the compat branch survives at all).

4. Collection membership is 16 collections / 100 memberships

Recorded with the full product list, option values, weights and product types in assets/dev-store-seed.json — the baseline the seed is checked against, and the membership map whichever mechanism is chosen will consume.

Nothing in the CSV path can carry this. The docs confirm the Collection column takes one collection per product, is never exported, and — per the overwrite rules — a blank non-required column overwrites the existing value as blank. Repeated imports with different Collection values would have to accumulate membership, which is undocumented and whose failure mode is destroying membership. Not a sound mechanism.

5. The second mechanism, at 2026-10

  • collectionAddProducts is deprecated; the current path is collectionUpdate with inclusion.selectionsToAdd, or collectionCreate with sources[].inclusion.selections. Both need write_products.
  • Trap: collectionCreate leaves the collection unpublished, so without a publishablePublish the membership work is invisible to the Storefront API — the thing the dev store exists to serve.

Decision: CSV for products, a dev-store app for collections ✅

Products, metafields and images stay on CSV — it re-hosts images for free, whereas an API seed would need staged uploads for 61 products. Collections go through a custom app installed in the dev store, driven by scripts/seed-dev-store.mjs, which creates each collection with its members in one call and publishes it. Chosen over hand-building because the membership map is a snapshot that will drift, and over an all-API seed because images would then need staging. It also makes the collection step re-runnable whenever production changes.

Fidelity gap, recorded rather than hidden: production's collection structure is unreadable with today's scopes, so the rebuild reproduces today's membership as manual collections. If any of the 16 is a smart collection in production, the dev store will hold a manual copy of it.

Runbook (HITL — the parts that need admin access)

In the dev store staging-iw0gviik.myshopify.com:

  1. Create the custom app. Settings → Apps and sales channels → Develop apps → Create an app → Configure Admin API scopes: read_products, write_products, read_publications, write_publications → Install app → copy the Admin API access token (shpat_…). write_products covers collectionCreate; write_publications and read_publications cover publishablePublish and the publications query.
  2. Create the metafield definitions (Settings → Custom data → Products) before importing — the 10 keys and types in §3 above. Undefined metafields on import are undocumented, which is why this comes first.
  3. Export from production, then import. Products → Export → Plain CSV → Export products (an All products export is emailed, not downloaded). Then Products → Import → Overwrite products with matching handles, small batch first. Leave Option1 name/Option1 value exactly as exported (§1). Images are URLs only: production's products must stay publicly reachable until the import finishes.
  4. Add the dev-store credentials to .env (gitignored): SHOPIFY_DEV_STORE_DOMAIN, SHOPIFY_DEV_ADMIN_TOKEN.
  5. Run the collection seed — dry run, then apply: npm run seed:dev-store, then npm run seed:dev-store -- --apply. It creates the 16 collections, adds the 100 memberships and publishes each one; existing handles are skipped and any product handle it cannot find is reported, which is how a partial CSV import shows up.
  6. Record what landed (To resolve #5): product count, collection count and membership, metafield coverage, anything rebuilt by hand — and whether the Variant GIDs differ from assets/dev-store-seed.json (they will).

Unverified until step 5 runs: the script's mutation shapes come from the 2026-10 Admin reference (it uses the CollectionCreateInput.sources form because collectionAddProducts is deprecated). Its auth, error and snapshot paths have been exercised against production; the writes have not, because no dev-store app exists yet. That is why the dry run is the default.

Facts later tickets depend on once this is done: which products exist in the dev store, the dev store's own Variant GIDs, and whether the metafield values survived the import.