Skip to Content
SimulatorsShopify Storefront changelog

Simulators

Shopify Storefront changelog

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.

Last updated on