Simulators
Shopify Storefront changelog
What changed in each Shopify Storefront simulator release, newest first.
v0.0.4 — 2026-11-06
- 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
-
A code discount’s allocation answers every field, so selecting one no longer empties the cart. On a cart with a discount code, each
CartCodeDiscountAllocationanswerstargetType(LINE_ITEM),discountApplicationandsourceDiscountApplicationwith Shopify’s allocation method, target selection, target type andPricingPercentageValue, besidecodeanddiscountedAmount, oncartCreate, every cart mutation andcart(id:). Before, selecting any of the three answeredcart: nullwith an error. -
A cart’s delivery group lists its lines and the whole delivery address.
deliveryGroups[].cartLineslists every line of the cart.deliveryAddressanswers the selected address’s street, names, company, phone,name,formattedandformattedArea, withcountryandprovincecarrying the codes andcountryCodeV2andprovinceCodenull, as Shopify answers them. ACartDeliveryAddressanswersname, aformattedlist ending in the country’s name, andformattedArea. Before,cartLineswas empty, onlycityandzipanswered, andformattedwas[]. -
A cart line write refuses a negative quantity.
cartLinesAddandcartLinesUpdatewith a quantity below 0 answer aCartUserErrorINVALIDon that line’squantityand change nothing, as Shopify does:cartLinesAddanswerscart: null, andcartLinesUpdateanswers the cart as it was. Before,cartLinesUpdatestored the negative quantity andcartLinesAddsubtracted it from a line the cart held. -
Removing or updating a line the cart does not hold answers Shopify’s error.
cartLinesRemoveandcartLinesUpdatenaming a line the cart no longer holds, or another cart’s line, answerINVALID_MERCHANDISE_LINEbeside the unchanged cart. A line id without its?cart=part, such asgid://shopify/CartLine/nope, answers Shopify’s top-levelRESOURCE_NOT_FOUNDInvalid iderror. Nothing is removed or updated when any id is refused. Before,cartLinesRemoveanswered HTTP 501 andcartLinesUpdateanswered success. -
Each
Countryunderlocalizationanswers its market.availableCountries[].marketandcountry.marketanswer theidandhandleof the market serving that country, the onelocalization.marketanswers, in every shipped scenario. Before, a scenario whose countries carried no market answered an emptyidandhandle. -
A pickup location’s address answers its country.
Location.address.countryanswers the country’s name (United States) besidecountryCode, onlocationsand a variant’sstoreAvailability, in every shipped scenario. Before, a location record without a country name answerednull. -
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.
-
The call log shows what each call asked for and whether it succeeded. Each
GET /_rystic/callsentry now carriesargs, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), andok, 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’serror. A path the simulator does not serve is markedunknown_route: true.?tail=Nreturns only the last N calls, anddroppedcounts the calls the log no longer keeps. -
The docs are one page, with a page per feature. The archive ships
README.mdin place ofHOWTO.md, with the same sections in the same order as every Rystic simulator’s page, andCHANGELOG.mdbeside it. Carts and the catalog with its search each have their own page underdocs/features/, and every example on these pages runs as written against a fresh simulator. -
The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.
-
Scenario descriptions use customer terms. The Scenarios table and
--list-scenariosdescribe each scenario in American spelling, with no internal references. -
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.
-
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”.
-
The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.
-
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
-
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 aGeoCoordinateInputwith a bad or missing field, now answersargumentLiteralsIncompatible,missingRequiredInputObjectAttributeorargumentNotAcceptedwith Shopify’s message, path and location. Before, the query ran with the value. -
A cart for a buyer in a country no market serves holds its lines at quantity 0.
cartCreateandcartBuyerIdentityUpdatewith acountryCodeoutsidelocalization.availableCountrieskeep each line at quantity 0, with 0.0 line and cart totals, and answer oneMERCHANDISE_OUT_OF_STOCKwarning 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. -
An
@inContextrequest answersextensions.context, andlocalizationhonors the directive. A request carrying@inContext(country:, language:)answersextensions.contextwith 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.localizationanswers that resolved country, language and market. Before, nocontextextension came andlocalizationignored the directive. -
Local pickup answers as Shopify does. A variant’s
storeAvailabilitylists each of the store’s locations, available, “Usually ready in 2 hours”, with the variant’squantityAvailable.buyerIdentity.preferencesis stored and read back oncartCreateandcartBuyerIdentityUpdate, merged member by member, and aPICK_UPdelivery preference leaves the cart no delivery group until the buyer prefersSHIPPINGagain. ALocation’s address answers every field its record holds (address1,province,zip,phoneand the rest). Before,storeAvailabilitywas empty,preferenceswasnulland a pickup buyer still got Standard and Express shipping. -
localization.availableCountrieslists each country in full, and a cart line answers its compare-at price. EachavailableCountriesentry answersname,currency,unitSystem,availableLanguagesanddefaultLanguageaslocalization.countrydoes; before, an emptynameand anullcurrencynulled the whole answer. A cart line’scost.compareAtAmountPerQuantityanswers its variant’s compare-at price, andnullonly for a variant without one. -
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’sproductsand a variant’sproductnow answerpriceRange,compareAtPriceRange, each option’soptionValues,variantBySelectedOptionsandselectedOrFirstAvailableVariantasproduct(id:|handle:)does. Before, the lookups answerednull,optionValuesanswered[], and a missing price range nulled the wholeproductsanswer. -
locationslists the store’s pickup locations. Thelocationsquery answers each location that offers in-store pickup, with its name and address, as a connection paged with Shopify’s cursors throughfirst,last,afterandbefore, ordered bysortKey(IDby default, orCITYorNAME) and flipped byreverse. A store with no pickup location answers an empty list.sortKey: DISTANCEanswers theIDorder, since the twin ignoresnear. Before,locationswas not served. -
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.mdstates the differences this release ships with: among them, a cart created with a code discount answers no cart when the allocation’stargetTypeor discount application is selected, a cart line write accepts a negative quantity, a cart’s delivery group answers no lines and a partial address,nodeanswers no Collection, and some search and list arguments are ignored.
v0.0.1 — 2026-10-23
-
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.jsonon one port, from a model validated against a live Shopify store. Point Shopify’s Storefront API client at it through itscustomFetchApioption (HOWTO.mdshows how), or curl it with any token value; no Shopify store, app or token is involved, and nothing reaches Shopify. -
The catalog, read as a storefront reads it.
productby ID or handle,productByHandleandproductsanswer each product with its variants, itsoptionswith theirvaluesin the shop’s order, and its price ranges, and every other root that answers a product —node,nodes, a collection’s products,search,predictiveSearch,productRecommendationsand a variant’sproduct— lists its variants the same way. Each variant answers its price,compareAtPrice, SKU, stock,selectedOptionsand product, throughnodeandnodestoo.productalone answers its variant lookups:variantBySelectedOptions(selectedOptions:)answers the variant whose options equal the pairs you send, in any order, andnullwhen none does (ignoreUnknownOptionsandcaseInsensitiveMatchloosen the match), andselectedOrFirstAvailableVariant(selectedOptions:)answers the same match, else the first available variant; every other root that answers a product answersnullfor both, and[]for an option’soptionValues.collection,collectionByHandleandcollectionslist their products in the store’s order;search,predictiveSearch,productRecommendationsandproductTagsanswer from the store’s products; every connection pages with Shopify’s cursors. An unknown ID or handle answers the fieldnull, as Shopify does. -
Pages, blogs, menus and the shop’s settings.
page,pageByHandleandpages,blog,blogByHandle,blogs,articleandarticles,menuwith its nested items,metaobjectandmetaobjects,urlRedirects, andshop,paymentSettings,localizationandpublicApiVersionsanswer the store’s records. -
Carts, from the first line to the checkout URL.
cartCreateand 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 ascart(id:)reads it. A code the store holds is applied again on every later write, and an unknown code staysapplicable: falsewith itsDISCOUNT_NOT_FOUNDwarning, as Shopify does. A selected address and a line give the cart a delivery group with Standard and Express options, andcost.totalAmountcarries the discount and the selected option. The twin ends at the cart’scheckoutUrl. -
Any Storefront token works, and a request with none is refused. The twin answers a public token in
X-Shopify-Storefront-Access-Tokenor a delegate token inShopify-Storefront-Private-Token, whatever its value. A request with neither answers 400Online Store channel is locked.(BAD_REQUEST), as a password-protected Shopify store answers it. -
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) andshopify-storefront_fresh_store(an empty store with one empty cart).--scenarioboots one,POST /_rystic/scenarioloads one without a restart, andSCENARIOS.mddescribes each. Without one, the twin boots an empty store. -
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.mdstates the differences this release ships with: among them, a cart mutation on an unknown cart answers nouserErrors, 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.