# Shopify Storefront simulator

A stateful Shopify Storefront API simulator — the shop, products, variants and collections, search and recommendations, pages, blogs, menus and metaobjects, and the cart with its fifteen mutations, over the one GraphQL endpoint.

`shopify-storefront-twin` is a local, stateful simulator of the Shopify Storefront API, version 2026-10, as a buyer who is not signed in uses it — the shop, its products, variants and collections, search, predictive search and recommendations, pages, blogs, articles, menus and metaobjects, and the cart with its fifteen mutations — built from the API's published introspection, as Shopify's Hydrogen package vendors it.

## Quickstart

You need an API key from [Account → API keys](https://www.rystic.ai/account/api-keys) ([how](https://www.rystic.ai/docs/licensing.md#get-an-api-key)).

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                                                                                # your API key
rystic run shopify-storefront-twin -d --port 0 --scenario shopify-storefront_apparel_catalog   # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version
```

Expect `"service_hosts":["myshopify.com"]`. The examples below use `localhost:8080`; use the port `rystic run` printed. On Enterprise, `rystic login ./rystic-license.json` takes the place of the key ([license files](https://www.rystic.ai/docs/licensing.md#enterprise-license-files)).

List the products, then create a cart and read it back:

```bash
SF=(localhost:8080/api/2026-10/graphql.json -H 'X-Shopify-Storefront-Access-Token: placeholder' -H 'Content-Type: application/json')
curl -s "${SF[@]}" -d '{"query":"{ products(first: 5) { nodes { handle title } } }"}'
# -> {"data":{"products":{"nodes":[{"handle":"classic-tee","title":"Classic Tee"},…]}},…}

curl -s "${SF[@]}" -d '{"query":"mutation { cartCreate(input: {lines: [{merchandiseId: \"gid://shopify/ProductVariant/9002\", quantity: 2}]}) { cart { id totalQuantity } userErrors { code message } } }"}'
# -> {"data":{"cartCreate":{"cart":{"id":"gid://shopify/Cart/8ef910ca90bf4c9d8000000000000001?key=7c0a4019","totalQuantity":2},"userErrors":[]}},…}
curl -s "${SF[@]}" -d '{"query":"{ cart(id: \"gid://shopify/Cart/8ef910ca90bf4c9d8000000000000001?key=7c0a4019\") { totalQuantity } }"}'
# -> {"data":{"cart":{"totalQuantity":2}},…}
```

## Point your client at it

Shopify's clients build every URL on `https://<your-store>.myshopify.com`. **A request left unpointed goes to your real store.**

`@shopify/storefront-api-client` always builds an `https` URL from `storeDomain`, even when you pass `http://localhost:8080`, so hand it a fetch that rewrites the store's origin to the twin's and refuses anything else:

```js
import { createStorefrontApiClient } from "@shopify/storefront-api-client";
const twin = "http://localhost:8080";
const client = createStorefrontApiClient({
  storeDomain: "your-store.myshopify.com",
  apiVersion: "2026-10",
  publicAccessToken: "placeholder",
  customFetchApi: (url, init) => {
    const target = String(url).replace(/^https:\/\/your-store\.myshopify\.com/, twin);
    if (!target.startsWith(twin + "/")) throw new Error(`refusing ${target}`);
    return fetch(target, init);
  },
});
const { data } = await client.request(`{ products(first: 5) { nodes { handle } } }`);
```

Any other GraphQL client works the same way: POST JSON to `http://localhost:8080/api/2026-10/graphql.json` with the token header.

## Plans

`shopify-storefront-twin` is on Pro and Enterprise, not Free ([plans](https://www.rystic.ai/docs/licensing.md#plans)); on a Free key, `rystic pull shopify-storefront-twin` and a start are refused with `product_not_in_plan`. Both plans serve the whole Storefront surface below, the whole control plane, custom latency and fault injection included, and every world. Without `--scenario` the twin boots an **empty store** — the shop `Rystic Storefront`, its localization, API versions and the discount code `WELCOME10`, with no products — and `POST /_rystic/reset` wipes it back to that.

## Auth

Send the storefront's public access token in the `X-Shopify-Storefront-Access-Token` header. The twin also accepts a delegate token in `Shopify-Storefront-Private-Token` in its place, as the measured store answered the `shop` and `cart` reads. It accepts any token value in either header and checks no token shape, scope or expiry.

The twin models a store whose password page is on, as the store it is measured against is. A request carrying neither header answers 400 `{"errors":[{"message":"Online Store channel is locked.","extensions":{"code":"BAD_REQUEST"}}]}` on every query and mutation, before any field resolves, as a password-protected Shopify store answers it. A live store with no password page serves some reads without a token; the twin does not model that store.

## Coverage

<!-- BEGIN generated: served-operations (go run ./ingest/cmd/twindocs shopify-storefront; do not edit) -->

<!-- coverage-claim -->
1 route across 19 resources, counted from the sim's literal registrations.

GraphQL fields answer at `POST /api/2026-10/graphql.json`.

| Resource | Operations |
|---|---|
| API version (1) | `QueryRoot.publicApiVersions` |
| Article (2) | `QueryRoot.article`; `QueryRoot.articles` |
| Blog (3) | `QueryRoot.blog`; `QueryRoot.blogByHandle`; `QueryRoot.blogs` |
| Cart (6) | `Mutation.cartAttributesUpdate`; `Mutation.cartBuyerIdentityUpdate`; `Mutation.cartCreate`; `Mutation.cartDiscountCodesUpdate`; `Mutation.cartNoteUpdate`; `QueryRoot.cart` |
| Cart delivery address (4) | `Mutation.cartDeliveryAddressesAdd`; `Mutation.cartDeliveryAddressesRemove`; `Mutation.cartDeliveryAddressesReplace`; `Mutation.cartDeliveryAddressesUpdate` |
| Cart delivery group (1) | `Mutation.cartSelectedDeliveryOptionsUpdate` |
| Cart line (3) | `Mutation.cartLinesAdd`; `Mutation.cartLinesRemove`; `Mutation.cartLinesUpdate` |
| Cart metafield (2) | `Mutation.cartMetafieldDelete`; `Mutation.cartMetafieldsSet` |
| Collection (4) | `QueryRoot.collection`; `QueryRoot.collectionByHandle`; `QueryRoot.collections`; `QueryRoot.nodes` |
| Localization (1) | `QueryRoot.localization` |
| Location (2) | `Location.address`; `QueryRoot.locations` |
| Menu (1) | `QueryRoot.menu` |
| Metaobject (2) | `QueryRoot.metaobject`; `QueryRoot.metaobjects` |
| Page (3) | `QueryRoot.page`; `QueryRoot.pageByHandle`; `QueryRoot.pages` |
| Product (15) | `Product.compareAtPriceRange`; `Product.options`; `Product.priceRange`; `Product.selectedOrFirstAvailableVariant`; `Product.variantBySelectedOptions`; `QueryRoot.node`; `QueryRoot.predictiveSearch`; `QueryRoot.product`; `QueryRoot.productByHandle`; `QueryRoot.productRecommendations`; `QueryRoot.productTags`; `QueryRoot.productTypes`; `QueryRoot.products`; `QueryRoot.search`; `QueryRoot.sitemap` |
| Product variant (1) | `ProductVariant.storeAvailability` |
| Shop (2) | `QueryRoot.paymentSettings`; `QueryRoot.shop` |
| URL redirect (1) | `QueryRoot.urlRedirects` |

<!-- END generated: served-operations -->

The catalog: `product` by id or handle, `productByHandle` and `products`, each with its variants and its `options` with their `values`, and only `product(id:|handle:)` with its price ranges in every case, which other roots can answer `null`, nulling the nearest field above them that may be null, and with `variantBySelectedOptions`, `selectedOrFirstAvailableVariant` and each option's `optionValues`, which every other root that answers a product answers `null` and `[]`; `collection`, `collectionByHandle` and `collections` with their products in the store's order; `products(query:)` by `tag:`; `search`, `predictiveSearch`, `productRecommendations` and `productTags`. Content: pages, blogs and articles, menus with their nested items, metaobjects by `type`, URL redirects, and the shop, its payment settings, localization and API versions. Every connection pages by `first`, `last`, `after` and `before` with Shopify's cursors, and an unknown id or handle answers the field `null`. The cart: its fifteen mutations, `cartCreate` among them, each payload answering the stored cart exactly as `cart(id:)` reads it, with `userErrors` and `warnings` in Shopify's shapes and lines newest first. A code the store holds applies its percentage on every later write; an unknown code stays `applicable: false` and repeats its `DISCOUNT_NOT_FOUND` warning. A selected delivery address and a line give the cart a delivery group with Standard (8.00) and Express (15.00) options, Standard selected, and `cost.totalAmount` carries the discount and the selected option while `checkoutChargeAmount` carries neither. The twin ends at the cart's `checkoutUrl`, which serves nothing.

Measured against a live Shopify store: the full-suite round of 2026-10-23 (stamp `20261023T085537Z`) matched 93 of the suite's 96 probes, one of them inconclusive, against a target of 80%.

## Control plane

Every route under `/_rystic/` answers on both plans; [routes by plan](https://www.rystic.ai/docs/control-plane.md#routes-by-plan) lists them.

## Scenarios

Each world is pinned at `2026-02-02T00:00:00Z` and holds the discount code `WELCOME10`.

- `shopify-storefront_apparel_catalog`: A stocked storefront — four published products with eight variants, two collections, a nested main menu, two pages, a journal with two articles, a lookbook metaobject, two redirects and two locations — so catalogue, content and navigation reads all return real pages from boot.
- `shopify-storefront_checkout_cart`: A cart one step from payment — four units across three lines, a buyer identity, two saved delivery addresses (one selected), a delivery group with a chosen shipping option and two cart metafields — so cart reads and line-level updates have a populated cart without a setup write.
- `shopify-storefront_fresh_store`: A storefront configured but empty — shop settings, the supported API version, a localization and a single location, plus one brand-new cart holding nothing at all — so every catalogue and cart-line listing returns zero rows and a first write's effect is unmistakable.
- `shopify-storefront_sold_out_shelf`: A shelf standing in every stock state from boot — a hoodie with in-stock, two-left, oversell-enabled and fully sold-out variants, a tee withdrawn from sale, and a pre-order mug with zero on hand — so availability-guarded reads and adds have live targets without inventory writes.

## Seeding state

`POST /_rystic/seed` takes a register map — `state_product` and `state_product_variant` (keyed by gid; a variant names its `productId`), `state_collection` (its `productIds` list its members), `state_discount_code` (keyed by `code`, with a `percentage`), `state_cart` and its lines, addresses, delivery groups and metafields, and the content registers `GET /_rystic/registers` lists — and merges it into the running store; [Worlds](https://www.rystic.ai/docs/concepts/worlds.md) covers seed files, `GET /_rystic/registers` and the seed gate. A product a cart can hold is a `state_product` and a `state_product_variant` naming it:

```bash
curl -X POST localhost:8080/_rystic/seed -H 'Content-Type: application/json' -d '{
  "state_product": {"gid://shopify/Product/1": {"id": "gid://shopify/Product/1", "handle": "test-mug",
    "title": "Test Mug", "availableForSale": true}},
  "state_product_variant": {"gid://shopify/ProductVariant/11": {"id": "gid://shopify/ProductVariant/11",
    "productId": "gid://shopify/Product/1", "title": "Default Title", "availableForSale": true,
    "quantityAvailable": 5, "price": {"amount": "12.00", "currencyCode": "USD"}}}
}'
# -> 204; product(handle: "test-mug") then answers it
```

## Determinism & reset

Cart, line, delivery-group and delivery-option ids are sequences the reset rewinds, and a cart's `?key=` comes from the same seeded sequence, so the first cart after a reset has the same id every time. The clock starts at a pinned instant — `2026-01-01T00:00:00Z` empty, `2026-02-02T00:00:00Z` in every world — and stands still until `POST /_rystic/clock` sets or advances it. Two identical runs from the same reset give byte-identical answers. [Reset and determinism](https://www.rystic.ai/docs/concepts/reset-and-determinism.md) covers reset, the clock and the RNG.

## Egress

The Storefront API declares no outbound effects: no request sends an email or calls a webhook, and `GET /_rystic/egress` is always empty.

## Limitations

- Every answer carries `extensions.cost.requestedQueryCost`, always `1`; Shopify computes it from the query. The twin enforces no cost or rate limit: never a `THROTTLED` answer or a 429.
- A request body holds up to 1 MiB (`blob_cap_bytes` on `/_rystic/version`).
- `sitemap` is served but answers no items.

Left out on purpose: cart completion and payment, gift cards, Shop Pay payment requests, the legacy customer operations and Multipass, `cartClone`, `cartRemovePersonalData` and `cartBillingAddressUpdate`, and the Customer Account API. `@defer` is ignored.

<!-- BEGIN generated: known-gaps (go run ./ingest/cmd/twindocs shopify-storefront; do not edit) -->

**Known issues.**

- A cart mutation on a cart id that names no cart answers `cart: null` with no `userErrors`, where Shopify answers a `CartUserError` `INVALID` ("The specified cart does not exist"); only `cartLinesAdd` does
- The twin accepts a query deeper than 17 and an id that is not a `gid://`, answers a wrongly typed variable with an error lacking `extensions` `{value, problems}`, and answers request errors with its own status and wording, where Shopify answers `MAX_QUERY_DEPTH_EXCEEDED`, refuses the id, and answers an empty body 400, malformed JSON 415 and a body with no query 200; treat any `errors` entry as the refusal, whatever its status or wording
- Three of the seeded tee's six variants answer no `sku`, and twelve of the sixteen seeded variants answer `quantityAvailable: null`, where Shopify answers both; and four products answer `availableForSale: false` while each of their variants answers `true`, where Shopify derives a product's `availableForSale` from its variants
- A cart mutation on a cart id without its `?key=` applies the change and answers no error, where Shopify applies it and also answers a `CartUserError` `INVALID`; a mutation with the wrong key is not yet measured
- `cartMetafieldsSet` answers success for an `ownerId` that names no cart, where Shopify answers `INVALID_OWNER` with `metafields: null`, and for a one-character `key`, where Shopify answers `TOO_SHORT`
- A cart line write ignores Shopify's stock rules: the same variant twice on `cartCreate` gives two lines, `cartLinesUpdate` above stock is not capped with `MERCHANDISE_NOT_ENOUGH_STOCK`, and a sold-out variant adds a 0-quantity line; an oversell or pre-order variant can't be added, which is not yet measured on Shopify
- The deprecated `priceV2`, `compareAtPriceV2`, `weightUnit`, `Cart.estimatedCost` and `CartLine.estimatedCost` answer null, which nulls the product or cart, where Shopify answers a value for each
- `sitemap(type: PRODUCT)` answers `pagesCount: null` and no items, where Shopify answers a count of 1 and every product; the other sitemap types are not yet measured
- `blog.articleByHandle`, `metaobject.field(key:)`, `cart.attribute(key:)` and `cart.metafields(identifiers:)` answer null or empty, `cart.metafield(namespace, key)` ignores the key, and a cart metafield's `createdAt` and `updatedAt` answer null, nulling it, where Shopify answers by the arguments
- List arguments are partly ignored or misread: `query:` on `articles`, `collections` and `pages`, price-range terms on `products`, `sortKey` and `reverse` on `collection.products`, and `near` on `locations(sortKey: DISTANCE)`, which Shopify documents as ordering by distance (not yet measured), are ignored; `OR` and `NOT` on `products` and `search`, parentheses and a wildcard on `products`, `AND`, `-`, quoted phrases and a price term on `search` match the wrong records or none; a connection with neither `first` nor `last`, `first` above 250 on any connection but `products`, and a `predictiveSearch` `limit` above 10 answer data, where Shopify answers an error; send plain terms only, one query per alternative, then filter and sort the returned list in your agent
- Delivery options are fixed at Standard 8.00 and Express 15.00 whatever the cart holds, where Shopify's rates come from the shop's delivery profile and can be free
- A GraphQL object type can render differently depending on the field that returns it, and nothing checks the twin's answers against the schema, so a null in a non-null field or an enum value outside its type can reach your agent
- The twin answers GraphQL only as a `POST` of JSON at the one declared endpoint: `GET ?query=`, `application/graphql` and form bodies, other API versions and the unversioned path answer 400 or 404, where Shopify answers 200
- `productTypes` answers a fixed list of six types unioned with the types of the store's products, whatever the store holds (an empty store still answers the six), where Shopify lists only the types of the store's products
- `cartLinesRemove` with a line id the cart does not hold answers HTTP 501 with `X-Rystic-Marker: unmodeled_guard` and a body that does not parse (a runtime defect), and `cartLinesUpdate` with one answers success with no error, where Shopify answers the payload with a `CartUserError` (`INVALID_MERCHANDISE_LINE` on `cartLinesRemove`); send only line ids `cart.lines` lists
- A cart's delivery group answers `cartLines: []` and a `deliveryAddress` holding only `city` and `zip`, and a `CartDeliveryAddress` answers `formatted: []` with `formattedArea` and `name` null, where Shopify answers the group's lines and the full address; read lines from `cart.lines` and address fields from `cart.delivery.addresses`
- `search`'s `productFilters` answers a fixed availability and price filter set whatever filters the store configures, where Shopify answers the store's own filters (option, type, vendor), and a price filter in the `productFilters` input can treat a product as priced 0; filter by price from each product's `priceRange` instead
- `node` answers `null` for a collection or a page id, which `nodes(ids:)` resolves on Shopify, and `node` and `nodes` both answer `null` for a location id, though a location is a `Node` in Shopify's schema; `nodes(ids:)` resolves collections and pages, `collection(id:)` and `page(id:)` read them directly, and `locations` lists the store's locations
- The cart's line merge, stock cap, discount codes, delivery groups and totals match Shopify only where the cart scenarios compared against Shopify reach them; a combination outside those scenarios is unchecked
- On a cart with a code discount, selecting `targetType`, `sourceDiscountApplication` or `discountApplication` on a `CartCodeDiscountAllocation` answers `cart: null` with a non-null error, where Shopify answers the cart; select `code` and `discountedAmount` instead
- `cartLinesUpdate` takes a negative quantity (the line holds it and the cart's totals drop) and `cartLinesAdd` takes one without an error (subtracting it from a line the cart holds for that item, removing the line at 0 or below), where Shopify answers a `CartUserError` `INVALID` and leaves the cart unchanged; send quantities of 0 or more
- In a world whose localization countries carry no `market` (every shipped world but `apparel_catalog` and `fresh_store`), a `Country`'s `market` under `localization` answers an empty `id` and `handle`, which names no market; read the serving market from `localization.market`
- In a world whose location record stores no country name, a pickup `Location`'s `address.country` answers `null` while `countryCode` and `formatted` name the country; read `address.countryCode` or `address.formatted`

**Not served.** 28 of the 75 operations in the published spec are not served.

- Mutation, 15 of 41 served: `Mutation.cartBillingAddressUpdate`, `Mutation.cartClone`, `Mutation.cartGiftCardCodesAdd`, `Mutation.cartGiftCardCodesRemove`, `Mutation.cartGiftCardCodesUpdate`, `Mutation.cartPaymentUpdate`, `Mutation.cartPrepareForCompletion`, `Mutation.cartRemovePersonalData`, … and 18 more
- QueryRoot, 32 of 34 served: `QueryRoot.cartCompletionAttempt`, `QueryRoot.customer`

**Not measured.** The fidelity suite runs 157 probes, and 6 documented branches have none. 1 needs a credential the measuring account does not hold (401 ×1). The other 5:

| Operation | Status | Why |
|---|---:|---|
| `QueryRoot.cart` | 200 | the model describes no input that reaches it |
| `QueryRoot.pageByHandle` | 200 | the model describes no input that reaches it |
| `QueryRoot.product` | 200 | the model describes no input that reaches it |
| `QueryRoot.products` | 200 | the model describes no input that reaches it |
| `QueryRoot.shop` | 200 | the model describes no input that reaches it |

**Not modeled.** What the model's operations state they leave out:

- address1, province, zip, phone, latitude and longitude on the oracle store: its one location holds no street address, so only countryCode, formatted and the null city and provinceCode are measured; the shipped worlds' values are answered as their records hold them. (`Location.address`)
- The answer to a cartId naming no cart (Shopify reports cart-level failures as userErrors inside a 200 payload, with data.cartAttributesUpdate.cart null): no capture or wire observation of that envelope is in the bundle, so no error branch with a body discriminator can be grounded. Needs a recorded unknown-cart mutation response. (`Mutation.cartAttributesUpdate`)
- The answer to more than 250 attributes, which the field description forbids: no recorded error envelope, so the rejection's code, message and field path are unknown. (`Mutation.cartAttributesUpdate`)
- Whether attributes are replaced wholesale or merged with existing cart attributes is not stated by the documentation in the bundle. (`Mutation.cartAttributesUpdate`)
- extensions.cost.requestedQueryCost on this mutation (the capture shows 14) is a cost-analysis value the model declares service-wide; it is ignored by this branch's field policies rather than claimed. (`Mutation.cartAttributesUpdate`)
- Invalid buyerIdentity values (a countryCode outside the CountryCode enum, a malformed email or phone, an unrecognised customerAccessToken, a companyLocationId the buyer may not use): documented as possible by the input type, but no recorded userErrors payload is in the bundle, so the userErrors-populated outcome is unmodeled. (`Mutation.cartBuyerIdentityUpdate`)
- The 250-value limits documented on deliveryAddressPreferences, preferences.delivery.deliveryMethod, preferences.delivery.pickupHandle and preferences.wallet have no recorded rejection, so no threshold guard or violating example is declared. (`Mutation.cartBuyerIdentityUpdate`)
- Unknown or malformed cartId: no capture or wire observation in the bundle shows how the service answers (CartUserError envelope with cart null, or a top-level errors entry), so no error branch, discriminator or existence guard is declared. Needs a recorded failing request before the branch can be modelled. (`Mutation.cartBuyerIdentityUpdate`)
- deliveryAddressPreferences (deprecated since 2025-01), and preferences.delivery.coordinates and pickupHandle values: typed in arguments_shape, but only deliveryMethod and wallet were measured (asm_cart_buyer_preferences, 2026-10-24); what coordinates or a pickupHandle do to the cart's delivery groups and to checkout prefill is unmodeled. (`Mutation.cartBuyerIdentityUpdate`)
- The upper bound on the number of lines/attributes/metafields/addresses (250 per input field, per CartInput) and the error the service answers above it are documented but not measured. (`Mutation.cartCreate`)
- buyerIdentity.countryCode effects on international pricing, and the @inContext directive: ExtensionsIncontextCart sends @inContext(country: CA) and the answer carries extensions.context country US and buyerIdentity.countryCode US, so the directive's effect on this shop is unresolved. (`Mutation.cartCreate`)
- input.discountCodes / input.giftCardCodes acceptance and the applicable flag, input.sourceName channel attribution, buyerIdentity.customerAccessToken and companyLocationId: no capture exercises them. (`Mutation.cartCreate`)
- quantity 0 on a line: the cart is created with totalQuantity 0 and no line, no userError and no warning (CartWarningsQuantityZero). It is a value variant of br_200_created with no body discriminator telling it apart, so it is not sealed as its own branch. (`Mutation.cartCreate`)
- Cart address cap: 'A cart can have up to 20 delivery addresses.' No observation shows the refusal of the 21st address. (`Mutation.cartDeliveryAddressesAdd`)
- Input list cap: 'The input must not contain more than 250 values.' No observation shows the refusal envelope. (`Mutation.cartDeliveryAddressesAdd`)
- Unknown or malformed cartId: the field documents a cart lookup but no capture or wire observation in the bundle shows the resulting envelope (a CartUserError code in userErrors, or a top-level errors entry), so no executable error branch and no existence guard are declared. Needs a recorded failing operation before an error branch with x_body_discriminator and x_status_equivalent can be sealed. (`Mutation.cartDeliveryAddressesAdd`)
- copyFromCustomerAddressId alternative of CartAddressInput: no in-scope operation creates a customer address and no capture exercises it. (`Mutation.cartDeliveryAddressesAdd`)
- validationStrategy STRICT: documented to reject an invalid address and leave the cart unchanged; no observation of the rejection payload is in the bundle. (`Mutation.cartDeliveryAddressesAdd`)
- The addressIds argument documents a 250-value cap; the service's answer to a list above that cap (a GraphQL errors envelope or a userError) is not observed in the bundle, so no refusal branch is declared. (`Mutation.cartDeliveryAddressesRemove`)
- The payload declares a userErrors list ([CartUserError!]!), but the bundle holds no recorded envelope for any failing case (unknown cartId, an addressId naming no address of that cart, a cart key that does not gate the delivery addresses). No executable error branch is declared until a capture or boundary observation grounds its discriminator pointer, code value and body. (`Mutation.cartDeliveryAddressesRemove`)
- … and 151 more

<!-- END generated: known-gaps -->

Release notes: [Changelog](https://www.rystic.ai/docs/changelog.md#shopify-storefront)

---

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