# Shopify Storefront changelog

What changed in each Shopify Storefront simulator release, newest first.

What changed in each Shopify Storefront simulator release, newest first.

## v0.0.4 — 2026-11-06

1. **The docs say what the one store holds.** The limitations list the products, variants and carts that belong to the store.

## v0.0.3 — 2026-11-05

1. **A code discount's allocation answers every field, so selecting one no longer empties the cart.** On a cart with a discount code, each `CartCodeDiscountAllocation` answers `targetType` (`LINE_ITEM`), `discountApplication` and `sourceDiscountApplication` with Shopify's allocation method, target selection, target type and `PricingPercentageValue`, beside `code` and `discountedAmount`, on `cartCreate`, every cart mutation and `cart(id:)`. Before, selecting any of the three answered `cart: null` with an error.

2. **A cart's delivery group lists its lines and the whole delivery address.** `deliveryGroups[].cartLines` lists every line of the cart. `deliveryAddress` answers the selected address's street, names, company, phone, `name`, `formatted` and `formattedArea`, with `country` and `province` carrying the codes and `countryCodeV2` and `provinceCode` `null`, as Shopify answers them. A `CartDeliveryAddress` answers `name`, a `formatted` list ending in the country's name, and `formattedArea`. Before, `cartLines` was empty, only `city` and `zip` answered, and `formatted` was `[]`.

3. **A cart line write refuses a negative quantity.** `cartLinesAdd` and `cartLinesUpdate` with a quantity below 0 answer a `CartUserError` `INVALID` on that line's `quantity` and change nothing, as Shopify does: `cartLinesAdd` answers `cart: null`, and `cartLinesUpdate` answers the cart as it was. Before, `cartLinesUpdate` stored the negative quantity and `cartLinesAdd` subtracted it from a line the cart held.

4. **Removing or updating a line the cart does not hold answers Shopify's error.** `cartLinesRemove` and `cartLinesUpdate` naming a line the cart no longer holds, or another cart's line, answer `INVALID_MERCHANDISE_LINE` beside the unchanged cart. A line id without its `?cart=` part, such as `gid://shopify/CartLine/nope`, answers Shopify's top-level `RESOURCE_NOT_FOUND` `Invalid id` error. Nothing is removed or updated when any id is refused. Before, `cartLinesRemove` answered HTTP 501 and `cartLinesUpdate` answered success.

5. **Each `Country` under `localization` answers its market.** `availableCountries[].market` and `country.market` answer the `id` and `handle` of the market serving that country, the one `localization.market` answers, in every shipped scenario. Before, a scenario whose countries carried no market answered an empty `id` and `handle`.

6. **A pickup location's address answers its country.** `Location.address.country` answers the country's name (`United States`) beside `countryCode`, on `locations` and a variant's `storeAvailability`, in every shipped scenario. Before, a location record without a country name answered `null`.

7. **A slow client no longer holds up the others.** The simulator reads a request's whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients' requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

8. **The call log shows what each call asked for and whether it succeeded.** Each `GET /_rystic/calls` entry now carries `args`, the call's query and body arguments with credentials redacted (long values cut, file contents left out), and `ok`, false for an answer that is not 2xx. A GraphQL answer at HTTP 200 that reports a refusal (a record that is not there, a user error or a listed error) is not ok either, and the error's text, where it carries one, is the entry's `error`. A path the simulator does not serve is marked `unknown_route: true`. `?tail=N` returns only the last N calls, and `dropped` counts the calls the log no longer keeps.

9. **The docs are one page, with a page per feature.** The archive ships `README.md` in place of `HOWTO.md`, with the same sections in the same order as every Rystic simulator's page, and `CHANGELOG.md` beside it. Carts and the catalog with its search each have their own page under `docs/features/`, and every example on these pages runs as written against a fresh simulator.

10. **The docs page links the admin token section.** The page now sends you to the admin token's section of the shared-simulators page.

11. **Scenario descriptions use customer terms.** The Scenarios table and `--list-scenarios` describe each scenario in American spelling, with no internal references.

12. **Pro starts with a 14-day trial.** Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.

13. **Limitations is shorter to read.** It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind "Show all limitations".

14. **The page has no Plans section.** The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.

15. **The docs read more plainly, and they mark where the simulator differs from Shopify.** Sentences are shorter, and a behavior the pages describe that differs from Shopify, such as the fixed delivery rates or the ignored defer directive, is marked "Differs from Shopify" beside it. What each cart write does to the delivery group, the cart's cost fields and where the clock starts are tables, and the known issues use the same plain wording. Auth, seeding and reset keep only what is particular to this simulator and link the shared pages for the rest.

## v0.0.2 — 2026-10-24

1. **A GraphQL argument written with a value of the wrong type is refused as Shopify refuses it.** A literal in the query that its argument's declared type does not hold, such as `locations(sortKey: BOGUS)`, `first: "5"` or a `GeoCoordinateInput` with a bad or missing field, now answers `argumentLiteralsIncompatible`, `missingRequiredInputObjectAttribute` or `argumentNotAccepted` with Shopify's message, path and location. Before, the query ran with the value.

2. **A cart for a buyer in a country no market serves holds its lines at quantity 0.** `cartCreate` and `cartBuyerIdentityUpdate` with a `countryCode` outside `localization.availableCountries` keep each line at quantity 0, with 0.0 line and cart totals, and answer one `MERCHANDISE_OUT_OF_STOCK` warning per line naming the product and variant. The lines stay at 0 after the buyer moves back to a served country, and each later buyer update repeats the warning, as on Shopify. Before, the lines kept their quantities and no warning came.

3. **An `@inContext` request answers `extensions.context`, and `localization` honors the directive.** A request carrying `@inContext(country:, language:)` answers `extensions.context` with the country and language the store resolved, as Shopify does: the directive's country when a market of the world serves it, else the store's own, and likewise for the language. `localization` answers that resolved country, language and market. Before, no `context` extension came and `localization` ignored the directive.

4. **Local pickup answers as Shopify does.** A variant's `storeAvailability` lists each of the store's locations, available, "Usually ready in 2 hours", with the variant's `quantityAvailable`. `buyerIdentity.preferences` is stored and read back on `cartCreate` and `cartBuyerIdentityUpdate`, merged member by member, and a `PICK_UP` delivery preference leaves the cart no delivery group until the buyer prefers `SHIPPING` again. A `Location`'s address answers every field its record holds (`address1`, `province`, `zip`, `phone` and the rest). Before, `storeAvailability` was empty, `preferences` was `null` and a pickup buyer still got Standard and Express shipping.

5. **`localization.availableCountries` lists each country in full, and a cart line answers its compare-at price.** Each `availableCountries` entry answers `name`, `currency`, `unitSystem`, `availableLanguages` and `defaultLanguage` as `localization.country` does; before, an empty `name` and a `null` `currency` nulled the whole answer. A cart line's `cost.compareAtAmountPerQuantity` answers its variant's compare-at price, and `null` only for a variant without one.

6. **A product answers its price ranges, option values and variant lookups on every query that returns it.** `productByHandle`, `products`, `node`, `nodes`, `search`, `predictiveSearch`, `productRecommendations`, a collection's `products` and a variant's `product` now answer `priceRange`, `compareAtPriceRange`, each option's `optionValues`, `variantBySelectedOptions` and `selectedOrFirstAvailableVariant` as `product(id:|handle:)` does. Before, the lookups answered `null`, `optionValues` answered `[]`, and a missing price range nulled the whole `products` answer.

7. **`locations` lists the store's pickup locations.** The `locations` query answers each location that offers in-store pickup, with its name and address, as a connection paged with Shopify's cursors through `first`, `last`, `after` and `before`, ordered by `sortKey` (`ID` by default, or `CITY` or `NAME`) and flipped by `reverse`. A store with no pickup location answers an empty list. `sortKey: DISTANCE` answers the `ID` order, since the twin ignores `near`. Before, `locations` was not served.

8. **Measured against a live Shopify store.** The full-suite round of 2026-10-24 (`20261024T184848Z`) matched 147 of 156 probes against a live Shopify store, with one more inconclusive. `HOWTO.md` states the differences this release ships with: among them, a cart created with a code discount answers no cart when the allocation's `targetType` or discount application is selected, a cart line write accepts a negative quantity, a cart's delivery group answers no lines and a partial address, `node` answers no Collection, and some search and list arguments are ignored.

## v0.0.1 — 2026-10-23

1. **A local Shopify storefront for your storefront code and the agents that shop it.** The first release of the Shopify Storefront twin: the Storefront API, version 2026-10, as a buyer who is not signed in uses it — the shop, its products, variants and collections, search and recommendations, pages, blogs, menus and metaobjects, and the cart with its fifteen mutations — answered at `POST /api/2026-10/graphql.json` on one port, from a model validated against a live Shopify store. Point Shopify's Storefront API client at it through its `customFetchApi` option (`HOWTO.md` shows how), or curl it with any token value; no Shopify store, app or token is involved, and nothing reaches Shopify.

2. **The catalog, read as a storefront reads it.** `product` by ID or handle, `productByHandle` and `products` answer each product with its variants, its `options` with their `values` in the shop's order, and its price ranges, and every other root that answers a product — `node`, `nodes`, a collection's products, `search`, `predictiveSearch`, `productRecommendations` and a variant's `product` — lists its variants the same way. Each variant answers its price, `compareAtPrice`, SKU, stock, `selectedOptions` and product, through `node` and `nodes` too. `product` alone answers its variant lookups: `variantBySelectedOptions(selectedOptions:)` answers the variant whose options equal the pairs you send, in any order, and `null` when none does (`ignoreUnknownOptions` and `caseInsensitiveMatch` loosen the match), and `selectedOrFirstAvailableVariant(selectedOptions:)` answers the same match, else the first available variant; every other root that answers a product answers `null` for both, and `[]` for an option's `optionValues`. `collection`, `collectionByHandle` and `collections` list their products in the store's order; `search`, `predictiveSearch`, `productRecommendations` and `productTags` answer from the store's products; every connection pages with Shopify's cursors. An unknown ID or handle answers the field `null`, as Shopify does.

3. **Pages, blogs, menus and the shop's settings.** `page`, `pageByHandle` and `pages`, `blog`, `blogByHandle`, `blogs`, `article` and `articles`, `menu` with its nested items, `metaobject` and `metaobjects`, `urlRedirects`, and `shop`, `paymentSettings`, `localization` and `publicApiVersions` answer the store's records.

4. **Carts, from the first line to the checkout URL.** `cartCreate` and the cart's mutations add, update and remove lines, apply discount codes, set the buyer identity, note, attributes and metafields, add, update, remove and replace delivery addresses, and select a delivery option, and every payload answers the stored cart exactly as `cart(id:)` reads it. A code the store holds is applied again on every later write, and an unknown code stays `applicable: false` with its `DISCOUNT_NOT_FOUND` warning, as Shopify does. A selected address and a line give the cart a delivery group with Standard and Express options, and `cost.totalAmount` carries the discount and the selected option. The twin ends at the cart's `checkoutUrl`.

5. **Any Storefront token works, and a request with none is refused.** The twin answers a public token in `X-Shopify-Storefront-Access-Token` or a delegate token in `Shopify-Storefront-Private-Token`, whatever its value. A request with neither answers 400 `Online Store channel is locked.` (`BAD_REQUEST`), as a password-protected Shopify store answers it.

6. **Four stores to boot straight into.** `shopify-storefront_apparel_catalog` (a stocked catalog with collections, menus, pages and a blog), `shopify-storefront_checkout_cart` (a cart one step from payment, with lines, a buyer, addresses, a chosen shipping option and metafields), `shopify-storefront_sold_out_shelf` (variants in every stock state) and `shopify-storefront_fresh_store` (an empty store with one empty cart). `--scenario` boots one, `POST /_rystic/scenario` loads one without a restart, and `SCENARIOS.md` describes each. Without one, the twin boots an empty store.

7. **Measured against a live Shopify store.** The full-suite round of 2026-10-23 (`20261023T085537Z`) matched 93 of 96 probes against a live Shopify store, one of them inconclusive. `HOWTO.md` states the differences this release ships with: among them, a cart mutation on an unknown cart answers no `userErrors`, request-level errors answer with the twin's own status and wording, the stock rules on line writes are not enforced, some list arguments are ignored, and delivery rates are fixed whatever the cart holds.

---

Source: https://www.rystic.ai/docs/simulators/shopify-storefront/changelog · Markdown: https://www.rystic.ai/docs/simulators/shopify-storefront/changelog.md
