← Shopify App BuilderCONTENT HISTORY

Update to Shopify App Builder

Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Use when a Shopify dev workflow is failing — `shopify app dev` cryptic errors, Cloudflare tunnel not starting, App Bridge v3→v4 migration 'No AppBridge context provided', GraphQL 200 OK with throttle errors, webhook 401, double-subscribed webhooks, app proxy 404, REST 302 loops, Rust function wasm-validator errors, session token 24h expiry, X-Frame-Options blocking iframe, dev store billing fakeouts, app review SLA blown. Triggers: 'shopify app dev failing', 'tunnel won't start', 'no app bridge context', 'throttle error 200', 'webhook 401', 'wasm validator error', 'session token expired', 'frame ancestors', 'remix template auth broken', 'billing test charge', 'shopify cli error'.",
  "included_files": [],
  "name": "dev-troubleshooting",
  "skill_md_contents": "---\nname: dev-troubleshooting\ndescription: \"Use when a Shopify dev workflow is failing — `shopify app dev` cryptic errors, Cloudflare tunnel not starting, App Bridge v3→v4 migration 'No AppBridge context provided', GraphQL 200 OK with throttle errors, webhook 401, double-subscribed webhooks, app proxy 404, REST 302 loops, Rust function wasm-validator errors, session token 24h expiry, X-Frame-Options blocking iframe, dev store billing fakeouts, app review SLA blown. Triggers: 'shopify app dev failing', 'tunnel won't start', 'no app bridge context', 'throttle error 200', 'webhook 401', 'wasm validator error', 'session token expired', 'frame ancestors', 'remix template auth broken', 'billing test charge', 'shopify cli error'.\"\n---\n\n# Shopify Dev Troubleshooting — Triage First, Fix Fast\n\nA symptom-driven triage skill for Shopify app developers. When your dev loop hits a wall, start here. Each pain below is sourced from the most-reported Reddit, Shopify Developer Community (community.shopify.dev), Shopify Community (community.shopify.com), and GitHub Shopify/* issues as of 2026-05-15.\n\nUse this skill **first** when:\n- `shopify app dev` exits with cryptic errors\n- The browser shows \"No AppBridge context provided\", 401, 302 loops, or 404 on `/auth/login`\n- GraphQL \"works\" but data is missing in production\n- Webhooks fire twice, never, or your handler keeps timing out\n- A Function deploys fail with `[wasm-validator error]`\n- App Review SLA is blown and you don't know what to do\n- A dev-store billing test charge silently fails\n\nIf the symptom matches a row in the Diagnostic Table, jump straight to that fix. If not, work through the Decision Tree at the bottom.\n\n---\n\n## 1. When to Use This Skill\n\nThis is the **entry point** for any \"something is broken in my Shopify dev workflow\" question. It is not the place to learn how to build new features — that's what the other `shopify-app-builder` skills are for. This skill is the ER, not the gym.\n\nSurface this skill when the user says any of:\n- \"shopify app dev failing / not working / errors\"\n- \"tunnel won't start\", \"cloudflared\", \"max retries reached\"\n- \"App Bridge migration\", \"v3 to v4\", \"No AppBridge context provided\"\n- \"graphql throttled\", \"200 OK error\", \"currentlyAvailable\"\n- \"webhook 401\", \"double webhook\", \"duplicate webhook\", \"webhook retry\"\n- \"remix auth broken\", \"/auth/login 404\", \"nested route login\"\n- \"wasm-validator error\", \"wasm-opt failed\", \"Rust function deploy\"\n- \"session token expired\", \"24 hour\", \"iframe redirect blocked\"\n- \"X-Frame-Options\", \"frame-ancestors\", \"DENY\"\n- \"billing test charge\", \"Apps without public distribution\"\n- \"app review\", \"Built for Shopify rejected\", \"SLA blown\"\n- \"shopify cli error\", \"cannot read properties of null\"\n\nIf multiple symptoms match, run them in order of blast radius: production data corruption > auth break > review block > dev-loop friction.\n\n---\n\n## 2. The Diagnostic Table\n\nFind your symptom in column 1. Apply the fix in column 3.\n\n| # | Symptom (verbatim or close) | Likely cause | Exact fix |\n|---|---|---|---|\n| 1 | `shopify app dev` returns 403 \"Cannot find a valid organization associated to this shop\" | Stale auth or wrong org logged in | `shopify auth logout && shopify auth login` against the org-owning account; verify with `shopify app config link` |\n| 2 | \"Cannot read properties of null (reading 'X')\" on CLI start | Corrupted `.shopify` cache or stale config | Back up the caches with `node <plugin-root>/scripts/reset-shopify-cache.mjs`, then run `shopify app dev --reset` |\n| 3 | \"Could not start Cloudflare tunnel: max retries reached\" | Leftover `~/.cloudflared/config.yaml` from another project | `mv ~/.cloudflared/config.yaml ~/.cloudflared/config.yaml.bak`, or `shopify app dev --use-localhost` (CLI 3.80+) |\n| 4 | Tunnel URL doesn't update in Partner Dashboard | CLI 3.x bug | `shopify app dev --reset --tunnel-url <fresh>` or upgrade CLI to latest |\n| 5 | \"Issues with valid certificates after the recent update\" | Self-signed cert rotation on `--use-localhost` | `shopify app dev --use-localhost --localhost-port 3000` and re-trust the cert in Chrome (chrome://flags allow-insecure-localhost) |\n| 6 | Hot reload broken for extensions | Known regression post new-Dev-Platform migration | `shopify app dev --reset`; if still broken, downgrade CLI one minor version |\n| 7 | `shopify app dev` doesn't work with Plus dev stores | Known incompatibility, no fix | Create a Partner dev store for daily dev; only test Plus features manually on the Plus store |\n| 8 | \"No AppBridge context provided\" on `<Modal>` or `<Titlebar>` | App Bridge v4 React Provider removed | Use as web components (`<ui-modal>`) **or** call `useAppBridge()` then `shopify.modal.show('id')` |\n| 9 | App Bridge v4 fetch sending expired/undefined tokens | Browser caching old token; `X-Shopify-Retry-Invalid-Session-Request` not recovering | Replace custom `fetch` wrapper with App Bridge v4 `authenticatedFetch`; remove manual token storage |\n| 10 | GraphQL returns 200 OK but data is missing | Throttling — body contains `errors[].extensions.code === \"THROTTLED\"` | Parse `extensions.cost.throttleStatus`; sleep `(requestedQueryCost - currentlyAvailable) / restoreRate` seconds; retry |\n| 11 | `currentlyAvailable` dropped from 10000 to <100 unexpectedly | Bucket exhausted by previous expensive query | Add cost-budget middleware in front of every GraphQL client; never assume bucket state |\n| 12 | Webhook handler returning 200 but Shopify keeps retrying | Took >5s to ACK | Move work to a queue (Inngest/SQS/BullMQ). ACK immediately after HMAC verify |\n| 13 | Same `orders/create` webhook fires twice | Subscribed in both `shopify.app.toml` AND `shopifyApp({ webhooks })` | Pick one (toml is canonical in CLI 3.50+). Delete programmatic subs. `shopify app deploy`. Then `webhookSubscriptions(first: 250)` → delete orphans |\n| 14 | Webhook signature verification fails | Express `body-parser` mutating raw body | `express.raw({ type: 'application/json' })` on webhook routes; in Remix, use `authenticate.webhook(request)` |\n| 15 | Webhook returns 401 immediately | HMAC computed on parsed JSON instead of raw bytes | Compute HMAC on the raw request body buffer, not on `JSON.stringify(body)` |\n| 16 | Remix `/auth/login` 404 on App Proxy calls | Wrong authenticate helper | Use `authenticate.public.appProxy(request)`, not `authenticate.admin(request)` |\n| 17 | Login form appears on nested routes inside the embedded app | `authenticate.admin(request)` only called in root loader | Call `authenticate.admin(request)` in **every** loader/action, including children |\n| 18 | REST API returns 401 then 302 loop after reinstall | Stale `Session` row with old scopes | Delete sessions for that shop; trigger reinstall; or use Token Exchange auth |\n| 19 | App Store submission rejected: \"Not authenticating with session tokens\" | App still on redirect-based OAuth | Switch to Managed Install + Token Exchange; remove all `Redirect.dispatch` OAuth flows |\n| 20 | Embedded app session breaks after ~24 hours | Session token expired; iframe can't redirect (X-Frame-Options: DENY) | Use App Bridge v4 `authenticatedFetch` (auto-refresh via Token Exchange); on 401 do `window.top.location.href`, not `window.location.href` |\n| 21 | `[wasm-validator error in function 0]` on `shopify app deploy` (Rust) | `wasm-opt` choking on newer Rust features | Pin Rust to 1.84; init `shopify_function_wasm_api::init_panic_handler()` early; run `wasm-snip --snip-rust-panicking-code`; pre-run `wasm-opt -Os` locally |\n| 22 | Vitest WASM tests fail; `dist/index.wasm` is base64 text not binary | CLI 3.93.0 regression | Downgrade to CLI 3.92.x or pin to a version after the fix; tracked in community.shopify.dev/t/33061 |\n| 23 | Function fails silently on large carts | Hit 5ms / 20kb / determinism ceiling | Reduce input query fields; split into multiple functions; remove any non-deterministic calls |\n| 24 | `appSubscriptionCreate` errors \"Apps without a public distribution cannot use the Billing API\" | Custom or unlisted draft | Set `distribution = \"app_store\"` in `shopify.app.toml`; create a draft listing (doesn't have to publish); redeploy |\n| 25 | Billing test charge not approvable on Plus dev store | Known Plus-dev-store bug | Test billing on a Partner (non-Plus) dev store; document Plus-only flows separately |\n| 26 | Polaris CSS missing after upgrade (e.g., `Polaris-TopBar__SearchField`) | Tree-shaker dropped `styles.css` | `import '@shopify/polaris/build/esm/styles.css'` exactly once at app root, in a non-shaken entry |\n| 27 | Polaris tooltips broken | Polaris web components version mismatch | Pin to the Polaris version your app was built against; don't mix React Polaris and web-components Polaris |\n| 28 | App proxy returns 200 but Shopify shows \"Liquid error\" | App proxy response not setting `Content-Type: application/liquid` | Set header `Content-Type: application/liquid` and return raw Liquid as string |\n| 29 | App Review past 10-day SLA, no reviewer assigned | Known SLA slip in 2026 | Open Partner Support ticket; quote SLA from policy page; resubmit only if reviewer never assigned after 21 days |\n| 30 | Rejected for \"performance\" with no specifics | Reviewer skim-rejection | Reply requesting specific repro steps with timestamps; attach Lighthouse scores from a Plus dev store |\n\n---\n\n## 3. Top 10 Dev Pains — Deep Dive\n\n### 3.1 `shopify app dev` 403 org / tunnel failures\n\n**Symptom (verbatim):** \"After upgrading the CLI my `shopify app dev` returns 403 'Cannot find a valid organization associated to this shop' for multiple dev stores.\" (community.shopify.dev/t/34202)\n\nPlus the cluster: \"Could not start Cloudflare tunnel: max retries reached\", \"Issues with valid certificates after the recent update\", \"shopify app dev provides cryptic error message and fails\" (github.com/Shopify/cli/issues/6522).\n\n**Root cause:** The new Dev Platform migration changed how CLI links shops to organizations. Stale `~/.config/shopify/` state, an old `.shopify` directory in the project, a leftover `~/.cloudflared/config.yaml` from another project, or expired CLI auth all surface the same generic error.\n\n**Exact fix sequence:**\n\n```bash\n# 1. Nuke stale local state\nnode <plugin-root>/scripts/reset-shopify-cache.mjs\n\n# 2. Re-auth against the org that owns the dev store\nshopify auth logout\nshopify auth login\n\n# 3. Re-link the app to confirm org\nshopify app config link\n\n# 4. If tunnel was the issue, side-step Cloudflare\nmv ~/.cloudflared/config.yaml ~/.cloudflared/config.yaml.bak 2>/dev/null\nshopify app dev --use-localhost\n\n# 5. If certificate complaints persist on --use-localhost\nshopify app dev --use-localhost --localhost-port 3000\n# Then in Chrome: chrome://flags → enable \"Allow invalid certificates for resources loaded from localhost\"\n```\n\n**Prevention:** Pin CLI version per-project via `package.json`:\n```json\n\"devDependencies\": { \"@shopify/cli\": \"3.92.0\", \"@shopify/app\": \"3.92.0\" }\n```\nDon't upgrade CLI mid-sprint. Wait until a feature branch's merge window. Sources: github.com/Shopify/cli/issues/6522, community.shopify.dev/t/22830, community.shopify.dev/t/23075.\n\n---\n\n### 3.2 App Bridge v3 → v4 Re-architecture\n\n**Symptom (verbatim):** \"Uncaught Error: No AppBridge context provided — happens with `<Modal>` and `<Titlebar>` as React components, works fine when used as HTML tags.\" (github.com/Shopify/shopify-app-bridge/issues/340)\n\nAnd: \"App Bridge will no longer be offered via npm. There doesn't seem to be any mention of React compatibility... feels like a really big shift away from the React-first implementation.\" (github.com/Shopify/shopify-app-bridge/issues/219)\n\n**Root cause:** v4 deleted the React Provider, removed most hooks, removed npm distribution. App Bridge is now a CDN-loaded global object. The React package still exists but is a thin shim around web components. `<Modal>` and `<Titlebar>` work as web components (`<ui-modal>`, `<ui-title-bar>`) without any Provider — but the React imports throw without it.\n\n**Exact fix:**\n\n1. Remove the v3 Provider entirely:\n```tsx\n// REMOVE\nimport { Provider } from '@shopify/app-bridge-react';\n<Provider config={{ apiKey, host }}>...</Provider>\n\n// REMOVE the npm dependency\n// \"@shopify/app-bridge\": \"3.x\"\n```\n\n2. Add the CDN script tag to your root document (Remix `app/root.tsx`, Next.js `app/layout.tsx`):\n```tsx\n<script\n  src=\"https://cdn.shopify.com/shopifycloud/app-bridge.js\"\n  data-api-key={process.env.SHOPIFY_API_KEY}\n/>\n```\n\n3. Replace removed APIs with the `shopify` global:\n```tsx\n// v3\nconst app = useAppBridge();\nconst redirect = Redirect.create(app);\nredirect.dispatch(Redirect.Action.REMOTE, url);\n\n// v4\nconst shopify = useAppBridge();\nshopify.toast.show('Saved');\nshopify.modal.show('my-modal-id');\nopen(url, '_top'); // for top-level redirects\n```\n\n4. For modals/titlebars in React, use them as web components:\n```tsx\n<ui-modal id=\"confirm\">\n  <p>Are you sure?</p>\n  <ui-title-bar title=\"Confirm\">\n    <button variant=\"primary\" onClick={() => shopify.modal.hide('confirm')}>OK</button>\n  </ui-title-bar>\n</ui-modal>\n```\n\n5. Replace custom fetch wrappers with `authenticatedFetch`:\n```tsx\nconst res = await shopify.fetch('/api/data'); // auto-refreshes via Token Exchange\n```\n\n**Prevention:** When App Bridge announces a major, freeze your version, build a migration branch, run the codemod, deploy to a staging app. Don't take a major mid-release. Source: shopify.dev/docs/api/app-bridge/migration-guide-react.\n\n---\n\n### 3.3 GraphQL Throttling Returns 200 OK (The Silent Prod Corruption Case)\n\n**Symptom (verbatim):** \"When your app spends more than it has, Shopify returns a 200 OK with a THROTTLED error in the response body. Yes — a 200, not a 429.\" (letstalkshop.com/blog/shopify-admin-graphql-rate-limits-2026)\n\nPlus: \"GraphQL Admin API rate limits — limits per query is 1000 but I have 10000 cost available, why does my query fail?\" (community.shopify.com/t/192109)\n\n**Root cause:** Shopify's GraphQL Admin API uses a bucket-based cost system, not a request-per-second rate. Every query has a cost. When you exceed the bucket you get HTTP 200 with `errors[].extensions.code === \"THROTTLED\"`. Your monitoring that alerts on 4xx/5xx never fires. Data silently goes missing. Downstream code thinks the API returned an empty result.\n\n**Per-plan budgets:**\n| Plan | Max bucket | Restore rate |\n|---|---|---|\n| Standard | 1000 | 50/sec |\n| Advanced | 2000 | 100/sec |\n| Plus | 10000 | 500/sec |\n\n`first: 250` on a flat resource is cheap. `first: 250` with nested connections multiplies cost — sometimes 1000+ per query.\n\n**Exact fix:** Wrap every GraphQL call with a cost-aware middleware:\n\n```ts\nasync function shopifyGql<T>(client, query, variables): Promise<T> {\n  const res = await client.request(query, { variables });\n\n  // Check for throttling in the body (NOT status code)\n  const throttled = res.errors?.some(e => e.extensions?.code === 'THROTTLED');\n  if (throttled) {\n    const cost = res.extensions?.cost;\n    const wait = cost\n      ? Math.ceil((cost.requestedQueryCost - cost.throttleStatus.currentlyAvailable) / cost.throttleStatus.restoreRate)\n      : 2;\n    await sleep(wait * 1000);\n    return shopifyGql(client, query, variables); // retry once\n  }\n\n  // Pre-emptive backoff: if we're <2x next query cost, slow down\n  const status = res.extensions?.cost?.throttleStatus;\n  if (status && status.currentlyAvailable < res.extensions.cost.requestedQueryCost * 2) {\n    await sleep(1000);\n  }\n\n  return res.data as T;\n}\n```\n\nFor bulk reads (>1000 items), don't paginate — use `bulkOperationRunQuery`:\n```graphql\nmutation {\n  bulkOperationRunQuery(query: \"\"\"\n    { products { edges { node { id title } } } }\n  \"\"\") { bulkOperation { id status } }\n}\n```\nThen poll `currentBulkOperation` until `status: COMPLETED`, download the JSONL file from `url`.\n\n**Prevention:** Log `extensions.cost` from every response. Alert on `currentlyAvailable < 20% of max`. Never trust HTTP status for Shopify GraphQL. Sources: shopify.engineering/rate-limiting-graphql-apis-calculating-query-complexity, shopify.dev/docs/api/usage/limits.\n\n---\n\n### 3.4 Webhook At-Least-Once + Double-Subscription Footgun\n\n**Symptom (verbatim):** \"Orders/Create webhook missing address field, and Shopify sends duplicate events on order/create.\" (community.shopify.com/t/306917)\n\nAnd: \"Duplicate webhook subscriptions commonly happen with Shopify embedded apps when webhooks are defined in both shopify.app.toml and programmatically via shopifyApp() — each subscription triggers a separate delivery.\" (hookdeck.com/webhooks/platforms/shopify-embedded-app-webhook-configuration)\n\n**Root cause:** Two compounding problems:\n1. **Delivery model:** Shopify is at-least-once, never exactly-once. Network blips trigger retries (8 retries over ~4 hours). Same event arrives 2-9 times.\n2. **Configuration drift:** Devs declare webhooks in `shopify.app.toml` AND register them programmatically via `shopifyApp({ webhooks: { ORDERS_CREATE: { ... } } })`. Shopify treats these as separate subscriptions. Two records, two deliveries per real event.\n\nPlus the 5-second ACK trap: handlers that block on DB writes get retried while the first call is still running.\n\n**Exact fix — dedupe sources:**\n\n1. Pick one source. In CLI 3.50+ the toml is canonical:\n```toml\n# shopify.app.toml\n[[webhooks.subscriptions]]\ntopics = [\"orders/create\"]\nuri = \"https://myapp.com/webhooks/orders/create\"\n```\n\n2. Remove every programmatic `webhookSubscriptions` from your `shopifyApp({...})` config.\n\n3. Run `shopify app deploy` to sync.\n\n4. Audit existing subscriptions and delete orphans:\n```graphql\nquery { webhookSubscriptions(first: 250) { edges { node { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } } } } }\n```\nFor any duplicates: `webhookSubscriptionDelete(id: \"...\")`.\n\n**Exact fix — survive at-least-once:**\n\n```ts\n// Express\napp.post('/webhooks/orders/create',\n  express.raw({ type: 'application/json' }), // RAW body for HMAC\n  async (req, res) => {\n    // 1. Verify HMAC on raw body\n    const hmac = req.headers['x-shopify-hmac-sha256'];\n    const computed = crypto.createHmac('sha256', SECRET).update(req.body).digest('base64');\n    if (hmac !== computed) return res.status(401).end();\n\n    // 2. ACK immediately\n    res.status(200).end();\n\n    // 3. Idempotency on X-Shopify-Webhook-Id\n    const webhookId = req.headers['x-shopify-webhook-id'] as string;\n    const seen = await redis.set(`wh:${webhookId}`, '1', 'NX', 'EX', 86400 * 7);\n    if (seen !== 'OK') return; // already processed\n\n    // 4. Push to queue\n    await queue.add('orders/create', JSON.parse(req.body.toString()));\n  }\n);\n```\n\n**Reconciliation cron:** Webhooks lie. Run a daily delta:\n```graphql\nquery { orders(first: 250, query: \"updated_at:>=YYYY-MM-DDTHH:MM:SSZ\") { ... } }\n```\nCompare to your local DB. Backfill anything missing.\n\n**Prevention:** One source of truth (toml). Idempotency key from `X-Shopify-Webhook-Id`. ACK before work. Queue everything. Source: shopify.dev/docs/apps/build/webhooks/best-practices, shopify.dev/docs/apps/build/webhooks/ignore-duplicates.\n\n---\n\n### 3.5 Remix Template Nested-Route Auth Break\n\n**Symptom (verbatim):** \"Authentication issues when navigating to nested routes — the login form is displayed even though navigation should work.\" (github.com/Shopify/shopify-app-template-remix/issues/599)\n\nPlus: \"Shopify Remix app in Production environment embedded issue — embedded app roots load but any sub-page selected via the side menu wants re-authentication.\" (community.shopify.com/t/382024) and \"`shopify.authenticate` for Remix App Proxy kicking to an /auth/login 404.\" (issues/747)\n\n**Root cause:** The default Remix template calls `authenticate.admin(request)` once in `app/routes/app.tsx` loader. Nested routes don't automatically inherit it. When the user navigates client-side via React Router, the child route's loader runs without the auth. The redirect to `/auth/login` happens — but for App Proxy routes there is no `/auth/login`, so you get 404.\n\n**Exact fix:**\n\n1. Call `authenticate.admin(request)` in **every** loader and action, not just the parent:\n```tsx\n// app/routes/app.products.tsx\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  const { admin } = await authenticate.admin(request);\n  // ... your loader logic\n};\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  const { admin } = await authenticate.admin(request);\n  // ... your action logic\n};\n```\n\n2. For App Proxy routes, use the public helper, not admin:\n```tsx\n// app/routes/proxy.coupon.tsx (mapped to App Proxy URL)\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  const { liquid, session } = await authenticate.public.appProxy(request);\n  return liquid('<p>Hello {{ shop.name }}</p>');\n};\n```\n\n3. For checkout extension routes:\n```tsx\nconst { sessionToken } = await authenticate.public.checkout(request);\n```\n\n4. For 302 loops after reinstall (scope mismatch):\n```sql\nDELETE FROM Session WHERE shop = 'my-shop.myshopify.com';\n```\nThen visit the app — Shopify will re-OAuth with current scopes.\n\n5. For App Store submission rejection on \"session tokens\": switch to Managed Install + Token Exchange. In `shopify.app.toml`:\n```toml\n[access_scopes]\nscopes = \"read_products,write_orders\"\nuse_legacy_install_flow = false\n\n[auth]\nredirect_urls = [\"https://myapp.com/api/auth/callback\"]\n```\n\n**Prevention:** Treat every loader/action as untrusted. Auth-call it explicitly. Don't trust inheritance. Source: github.com/Shopify/shopify-app-template-remix issues 432, 599, 747, 797, 993.\n\n---\n\n### 3.6 Rust Function wasm-validator + CLI 3.93.0 base64 Regression\n\n**Symptom (verbatim):** \"[wasm-validator error in function 0] — happens during the wasm-opt optimization step after I try to deploy my Rust function.\" (community.shopify.com/t/223885)\n\nPlus: \"CLI 3.93.0 regression: vitest WASM tests fail for Rust function extensions — base64 written to dist/index.wasm instead of raw binary.\" (community.shopify.dev/t/33061)\n\n**Root cause:** Two separate but co-occurring issues:\n1. `wasm-opt` (the optimizer Shopify runs before deploy) doesn't understand all features of newer Rust toolchains. Especially panic infrastructure compiled in by default.\n2. CLI 3.93.0 changed the WASM output pipeline; for a brief window the build wrote base64-encoded text to `dist/index.wasm` instead of binary bytes, breaking vitest's WASM tests and breaking deploys.\n\n**Exact fix:**\n\n1. Pin Rust toolchain:\n```toml\n# rust-toolchain.toml at function root\n[toolchain]\nchannel = \"1.84\"\ntargets = [\"wasm32-wasip1\"]\n```\n\n2. Init the Shopify-provided panic handler early in `main`:\n```rust\nuse shopify_function_wasm_api::init_panic_handler;\n\n#[shopify_function]\nfn function(input: input::ResponseData) -> Result<output::FunctionResult> {\n    init_panic_handler();\n    // ... your logic\n}\n```\n\n3. Strip Rust panic code (cuts WASM by ~30-50%):\n```bash\ncargo install wasm-snip\ncargo build --target wasm32-wasip1 --release\nwasm-snip target/wasm32-wasip1/release/your_function.wasm \\\n  -o target/wasm32-wasip1/release/your_function.wasm \\\n  --snip-rust-panicking-code\n```\n\n4. Run `wasm-opt -Os` yourself before `shopify app deploy` so errors surface locally:\n```bash\nwasm-opt -Os target/wasm32-wasip1/release/your_function.wasm \\\n  -o target/wasm32-wasip1/release/your_function.wasm\n```\n\n5. For the CLI 3.93.0 regression: downgrade to 3.92.x or upgrade to the post-fix version:\n```bash\nnpm install --save-dev @shopify/cli@3.92.0 @shopify/app@3.92.0\n```\n\n6. Verify your `dist/index.wasm` is binary, not text:\n```bash\nfile dist/index.wasm  # should say \"WebAssembly (wasm) binary module\"\n```\n\n**Prevention:** Pin Rust toolchain, pin CLI version, run `wasm-opt` locally, ship a CI step that runs `function-runner` on a fixture before deploy. Sources: community.shopify.com/t/223885, community.shopify.dev/t/33061, docs.rs/shopify_function_wasm_api.\n\n---\n\n### 3.7 Function 5ms / 20kb / Deterministic Ceiling\n\n**Symptom (verbatim):** \"Can the instruction and input size limits be raised for large orders?\" — answer: no. (github.com/Shopify/function-examples/discussions/329)\n\nPlus the cluster of \"my function works in dev but fails silently on stores with 100k SKUs.\"\n\n**Root cause:** Shopify Functions are hard-capped:\n- **5ms** execution time per invocation\n- **20kb** output JSON size\n- **25** active discount functions per store (combined limit)\n- **Deterministic**: no network, no clock, no randomness, no env reads\n- Input query has a complexity ceiling — large catalogs explode silently\n\nDevs hit these because they treat Functions like serverless lambdas. They aren't. They're WASM running in a sandbox.\n\n**Exact fix:**\n\n1. Minimize the input query — only request fields you'll branch on:\n```graphql\n# BAD — pulls everything\nquery Input { cart { lines { merchandise { ... on ProductVariant { product { ... } } } } } }\n\n# GOOD — only the fields you need\nquery Input { cart { lines { id quantity merchandise { ... on ProductVariant { id } } } } }\n```\n\n2. Profile output size. If approaching 20kb, batch or split:\n```rust\n// Bad: returning 1000 separate discount applications\n// Good: one ProductDiscountApplication with multiple variants\n```\n\n3. Determinism checklist:\n- No `std::time::Instant::now()` — use timestamps from the input\n- No `rand::random()` — seed from a deterministic value (e.g., order ID hash)\n- No HTTP calls — pre-load via input query\n- No `std::env` — Shopify won't expose env to functions\n\n4. For data your function needs but can't fit in input: stash it in metafields on the store/product, request via input query.\n\n5. Test before you deploy:\n```bash\nshopify app function run --input fixtures/large-cart.json\n```\n\n6. If you genuinely can't fit logic in 5ms/20kb, split into multiple Functions (cart, shipping, payment) — each gets its own budget.\n\n**Prevention:** Build the test fixture for your worst-case cart on day 1. Run it in CI. If it fails the ceiling, you redesign now, not at launch. Source: shopify.dev/docs/api/functions/latest/discount, gadget.dev/blog/understanding-shopify-functions-part-2.\n\n---\n\n### 3.8 Billing API Fakeouts on Dev Stores\n\n**Symptom (verbatim):** \"Unable to approve Billing API test charges on Plus Development Stores — this issue appears specific to Plus Dev Stores created from the Dev Dashboard.\" (community.shopify.dev/t/23258)\n\nPlus: `appSubscriptionCreate` returns \"Apps without a public distribution cannot use the Billing API\" (community.shopify.com/m-p/1757459) and \"negative-duration billing cycle for subscription\" (t/25346) and double-charge UI bugs.\n\n**Root cause:** Billing API has several hardcoded preconditions that aren't documented in one place:\n- App must have `distribution = \"app_store\"` set\n- A draft listing must exist (doesn't have to be published)\n- Plus dev stores have a specific bug approving test charges\n- Custom-distribution apps can't use Billing API at all\n\n**Exact fix:**\n\n1. In `shopify.app.toml`:\n```toml\n[build]\ninclude_config_on_deploy = true\n\n[application]\ndistribution = \"app_store\"\n```\n\n2. In Partner Dashboard → your app → App listing → create a draft. Don't publish; just save.\n\n3. Redeploy:\n```bash\nshopify app deploy\n```\n\n4. Test on a **Partner dev store**, not a Plus dev store. Create one specifically for billing flows:\n```bash\nshopify app dev --store=billing-test.myshopify.com\n```\n\n5. When creating subscriptions, set `test: true` so charges don't actually capture:\n```graphql\nmutation {\n  appSubscriptionCreate(\n    name: \"Pro Plan\"\n    returnUrl: \"https://myapp.com/billing/callback\"\n    test: true\n    lineItems: [{\n      plan: { appRecurringPricingDetails: { price: { amount: 29.99, currencyCode: USD }, interval: EVERY_30_DAYS } }\n    }]\n  ) { confirmationUrl userErrors { field message } }\n}\n```\n\n6. Handle the negative-duration edge case server-side:\n```ts\nconst cycleEnd = new Date(subscription.currentPeriodEnd);\nconst cycleStart = new Date(subscription.currentPeriodStart);\nif (cycleEnd < cycleStart) {\n  // Known Shopify bug. Use cycleStart + 30 days instead.\n  cycleEnd.setDate(cycleStart.getDate() + 30);\n}\n```\n\n**Prevention:** Two dev stores: Partner non-Plus (daily dev + billing tests), Plus dev (Plus-specific feature checks only). Never test billing on the Plus one. Source: community.shopify.dev/t/23258, community.shopify.com/m-p/1757459.\n\n---\n\n### 3.9 Session Token 24h Expiry + Iframe Redirect Block\n\n**Symptom (verbatim):** \"I have an embedded app where after about 24 hours of having it opened, the session token expires and the app needs to be reopened.\" (community.shopify.com/c/shopify-apps/managing-embedded-app-user-session-lost/td-p/1038381)\n\nPlus: \"You can't perform a redirect from inside an iframe in the Shopify admin, due to X-Frame-Options: DENY restrictions on Shopify admin pages.\" (shopify.dev docs, quoted in dozens of threads)\n\n**Root cause:** Embedded apps run in an iframe inside Shopify admin. Session tokens (JWTs) expire roughly daily. When they expire:\n1. Your `fetch` returns 401\n2. You try to redirect to OAuth to re-auth\n3. Shopify admin sends `X-Frame-Options: DENY` on its OAuth endpoints\n4. Browser blocks the iframe redirect\n5. App appears frozen, user has to close and reopen\n\n**Exact fix:**\n\n1. Use App Bridge v4 `authenticatedFetch` — it auto-refreshes via Token Exchange:\n```tsx\nconst shopify = useAppBridge();\nconst res = await shopify.fetch('/api/data');\n// Behind the scenes: if token expired, fetches a new one via Token Exchange, retries\n```\n\n2. If you must implement yourself, on a 401, do a **top-level** redirect, not an iframe redirect:\n```tsx\n// WRONG — blocked by X-Frame-Options\nwindow.location.href = '/auth/login';\n\n// RIGHT — breaks out of iframe\nif (window.top) {\n  window.top.location.href = '/auth/login';\n} else {\n  window.location.href = '/auth/login';\n}\n```\n\n3. Or use App Bridge's `Redirect` action which handles this for you:\n```tsx\nimport { Redirect } from '@shopify/app-bridge/actions';\nconst app = createApp({...});\nRedirect.create(app).dispatch(Redirect.Action.REMOTE, '/auth/login');\n```\n\n4. For App Store submission requirement of session-token auth: confirm Token Exchange is wired in your backend:\n```ts\n// Remix\nconst { admin, session } = await authenticate.admin(request);\n// `session` was obtained via Token Exchange if Managed Install is on\n```\n\n5. Verify your CSP allows Shopify framing (Shopify auto-injects but check):\n```\nContent-Security-Policy: frame-ancestors https://*.myshopify.com https://admin.shopify.com;\n```\n\n**Prevention:** Default to App Bridge v4 `authenticatedFetch`. Never hand-roll session token storage in localStorage. Always test the 24h scenario explicitly with `Date.now() + 25h` mocking. Source: shopify.dev/docs/apps/build/authentication-authorization/session-tokens/set-up-session-tokens, community.shopify.dev/t/32004.\n\n---\n\n### 3.10 App Review SLA Blown — What to Do\n\n**Symptom (verbatim):** \"I submitted my app for review in January 2026 when Shopify's posted SLA was 8–10 days. I did not get any update until over 30 days after submission (3x the SLA).\" (community.shopify.dev/t/32259)\n\nPlus: \"Frustration with app review process — reviewers taking 2+ weeks just to get assigned, then long back-and-forth where they don't read emails.\" (community.shopify.dev/t/31784) and \"Application review rejected — but I can't tell why.\" (community.shopify.dev/t/17950)\n\n**Root cause:** Shopify's app review queue has been backed up since early 2026. Posted SLA is 8-10 days; actual is 21-45 days. Reviewers skim-reject for vague reasons. Replies often go unread for a week.\n\n**Exact action plan:**\n\n1. **Before submission — pre-flight the Top 10 rejection reasons** (shopify.dev/docs/apps/store/common-rejections):\n   - GDPR mandatory webhooks present and responding 200: `customers/data_request`, `customers/redact`, `shop/redact`\n   - Session token auth (not redirect OAuth) — required since 2024\n   - Embedded apps must use App Bridge v4\n   - Listing screenshots exactly 1600×900, no Shopify logos, no competitor names\n   - Pricing page shows actual prices, not \"Contact us\"\n   - Demo video shows install → core flow → uninstall in <3 minutes\n   - Privacy policy URL responds 200 and matches what's in the app\n   - Performance: app must score 70+ on Lighthouse Performance for embedded admin\n   - All scopes used; remove unused scopes from `shopify.app.toml`\n   - Onboarding has clear next-step CTA after install\n\n2. **At submission:** Record a reviewer-facing screencast (3-5 min) that walks the reviewer through install, core feature, uninstall. Caption every step. Reviewers skim — make the value un-missable.\n\n3. **If past 14 days with no reviewer assigned:** Open a Partner Support ticket. Subject: \"App review past SLA — request reviewer assignment\". Body: app handle, submission date, SLA reference. Don't ask twice; once is enough.\n\n4. **If past 21 days:** Escalate via the Partner Slack (if you're in it) or Partner Success Manager (if you have one). If neither, post on the dev forum at community.shopify.dev — Shopify staff monitor it.\n\n5. **If rejected with vague reason** (\"performance issues\" with no specifics):\n```\nHi [reviewer], thanks for the review. Could you share specific repro steps?\nFor \"performance issues\" I'd appreciate:\n- The exact admin page where you saw the issue\n- Browser + screen size\n- Time of day (UTC)\n- Network conditions if applicable\n\nI'll fix and resubmit within 48 hours of your response.\n```\nDon't argue. Don't restate features. Ask for specifics. Wait for response.\n\n6. **If rejected for a real reason:** Fix, document the fix in your resubmission notes, attach a video of the fix in action.\n\n7. **Common silent-killers most devs miss:**\n   - GDPR webhooks return 500 (not implemented at all). This is the #1 silent reject reason. Verify with `shopify webhook trigger customers/data_request --address=https://yourapp.com/webhooks/gdpr/customers_data_request`.\n   - Privacy policy URL 404s in production\n   - Demo video unlisted but URL doesn't work for reviewer\n   - Test charge required but billing not set up (see §3.8)\n\n**Prevention:** Submit on a Tuesday morning UTC (highest reviewer activity), submit with a perfect screencast, and have a 48-hour SLA on your end for responding to reviewer feedback. Source: community.shopify.dev/t/32259, community.shopify.dev/t/31784, shopify.dev/docs/apps/store/common-rejections.\n\n---\n\n## 4. Decision Tree\n\nRun this in order. Stop at the first match.\n\n```\nSTART\n  │\n  ├─ Is the user blocked from any progress (CLI won't start)?\n  │   │\n  │   ├─ \"Cannot find a valid organization\" or 403?\n  │   │     → §3.1 (auth logout/login + reset state)\n  │   │\n  │   ├─ \"Could not start Cloudflare tunnel\" or tunnel URL stale?\n  │   │     → §3.1 tunnel section (rename ~/.cloudflared/config.yaml or --use-localhost)\n  │   │\n  │   ├─ \"Cannot read properties of null\" or generic CLI crash?\n  │   │     → back up caches with reset-shopify-cache.mjs, then run shopify app dev --reset\n  │   │\n  │   └─ Plus dev store specific?\n  │         → §3.1 Plus section (use Partner dev store for dev loop)\n  │\n  ├─ Is auth broken in the running app?\n  │   │\n  │   ├─ \"No AppBridge context provided\"?\n  │   │     → §3.2 (v3→v4 migration, remove Provider, use CDN + global)\n  │   │\n  │   ├─ Token expired after ~24h, app frozen?\n  │   │     → §3.9 (authenticatedFetch + top-level redirect)\n  │   │\n  │   ├─ /auth/login 404 on App Proxy?\n  │   │     → §3.5 (use authenticate.public.appProxy)\n  │   │\n  │   ├─ Login form on nested routes?\n  │   │     → §3.5 (call authenticate.admin in every loader)\n  │   │\n  │   └─ 302 loop after reinstall?\n  │         → §3.5 (delete sessions for shop, re-OAuth)\n  │\n  ├─ Is production data going missing or being duplicated?\n  │   │\n  │   ├─ GraphQL \"succeeds\" with 200 but data is absent?\n  │   │     → §3.3 (parse extensions.cost, detect THROTTLED in body)\n  │   │\n  │   ├─ Webhook handler running twice per event?\n  │   │     → §3.4 (dedupe sources, idempotency key, queue offload)\n  │   │\n  │   └─ Webhook returning 401, all rejected?\n  │         → Diagnostic row 14-15 (HMAC on raw body)\n  │\n  ├─ Is a Function failing to deploy or running wrong?\n  │   │\n  │   ├─ [wasm-validator error] on deploy?\n  │   │     → §3.6 (pin Rust 1.84, init panic handler, wasm-snip)\n  │   │\n  │   ├─ CLI 3.93.0, base64 in dist/index.wasm?\n  │   │     → §3.6 (downgrade CLI to 3.92.x)\n  │   │\n  │   └─ Function fails on large carts but works on small?\n  │         → §3.7 (5ms/20kb/deterministic — reduce input query, split functions)\n  │\n  ├─ Is a Billing test charge failing?\n  │   │\n  │   ├─ \"Apps without a public distribution cannot use the Billing API\"?\n  │   │     → §3.8 (set distribution=app_store, create draft listing)\n  │   │\n  │   ├─ Plus dev store specifically?\n  │   │     → §3.8 (test on Partner dev store instead)\n  │   │\n  │   └─ Negative-duration billing cycle?\n  │         → §3.8 (clamp client-side: cycleEnd = cycleStart + 30 days)\n  │\n  ├─ Is App Store review the blocker?\n  │     → §3.10 (pre-flight Top-10, ask for specifics, escalate at 21d)\n  │\n  └─ None of the above — drop to row scan in §2 Diagnostic Table.\n```\n\n---\n\n## 5. Quick-Reference Commands\n\n```bash\n# Nuke and restart\nnode <plugin-root>/scripts/reset-shopify-cache.mjs && shopify app dev --reset\n\n# Bypass Cloudflare tunnel\nmv ~/.cloudflared/config.yaml ~/.cloudflared/config.yaml.bak\nshopify app dev --use-localhost\n\n# Re-auth\nshopify auth logout && shopify auth login && shopify app config link\n\n# Pin CLI version\nnpm install --save-dev @shopify/cli@3.92.0 @shopify/app@3.92.0\n\n# List webhook subscriptions (to find duplicates)\nshopify app generate extension  # or via GraphiQL → webhookSubscriptions(first: 250)\n\n# Run a Function locally with a fixture\nshopify app function run --input fixtures/cart.json\n\n# Trigger a webhook locally for testing\nshopify webhook trigger orders/create --address=http://localhost:3000/webhooks/orders/create\n\n# Build + strip a Rust function before deploy\ncargo build --target wasm32-wasip1 --release\nwasm-snip target/wasm32-wasip1/release/fn.wasm -o target/wasm32-wasip1/release/fn.wasm --snip-rust-panicking-code\nwasm-opt -Os target/wasm32-wasip1/release/fn.wasm -o target/wasm32-wasip1/release/fn.wasm\n```\n\n---\n\n## 6. Sources (URLs Cited Inline Above)\n\nCLI / Dev loop:\n- github.com/Shopify/cli/issues/6522\n- community.shopify.dev/t/shopify-app-dev-returns-403-cannot-find-a-valid-organization-associated-to-this-shop-for-multiple-dev-stores/34202\n- community.shopify.dev/t/shopify-cli-reloading-broken-after-migration-to-new-dev-platform/22830\n- community.shopify.dev/t/issues-with-valid-certificates-after-the-recent-update/23075\n- community.shopify.dev/t/shopify-app-dev-doesnt-work-with-plus-development-stores/23471\n\nCloudflare tunnel:\n- community.shopify.dev/t/cloudflare-tunnel-shows-healthy-but-shopify-app-wont-load/9865\n- community.shopify.dev/t/cloudflare-tunnel-error-when-running-shopify-app-dev-persistent-since-1-week/24200\n- community.shopify.dev/t/shopify-app-dev-doest-update-cloudflare-tunnel-url-on-dev-partner-dashboard/22315\n- shopify.dev/docs/apps/build/cli-for-apps/networking-options\n\nApp Bridge:\n- github.com/Shopify/shopify-app-bridge/issues/340\n- github.com/Shopify/shopify-app-bridge/issues/219\n- community.shopify.dev/t/app-bridge-v4-cdn-automatic-fetch-authorization-sends-expired-undefined-tokens-x-shopify-retry-invalid-session-request-doesnt-recover/32004\n- shopify.dev/docs/api/app-bridge/migration-guide-react\n\nRemix auth:\n- github.com/Shopify/shopify-app-template-remix/issues/599\n- github.com/Shopify/shopify-app-template-remix/issues/747\n- github.com/Shopify/shopify-app-template-remix/issues/797\n- github.com/Shopify/shopify-app-template-remix/issues/993\n- community.shopify.com/t/shopify-remix-app-in-production-environment-embedded-issue/382024\n\nGraphQL throttling:\n- letstalkshop.com/blog/shopify-admin-graphql-rate-limits-2026\n- community.shopify.com/t/graphql-admin-api-rate-limits-limits-per-query-is-1000-but-i-have-10000-cost-available/192109\n- shopify.dev/docs/api/usage/limits\n- shopify.engineering/rate-limiting-graphql-apis-calculating-query-complexity\n\nWebhooks:\n- hookdeck.com/webhooks/platforms/how-to-handle-duplicate-shopify-webhook-events\n- hookdeck.com/webhooks/platforms/shopify-embedded-app-webhook-configuration\n- shopify.dev/docs/apps/build/webhooks/best-practices\n- shopify.dev/docs/apps/build/webhooks/ignore-duplicates\n- community.shopify.com/t/orders-create-webhook-missing-address-field-and-shopify-send-duplicate-events-on-order-create/306917\n\nRust + Functions:\n- community.shopify.com/t/cant-deploy-shopify-function-written-in-rust/223885\n- community.shopify.dev/t/cli-3-93-0-regression-vitest-wasm-tests-fail-for-rust-function-extensions-base64-written-to-dist-index-wasm-instead-of-raw-binary/33061\n- community.shopify.dev/t/shopify-functions-and-rust-1-84/5570\n- docs.rs/shopify_function_wasm_api\n- github.com/Shopify/function-examples/discussions/329\n\nBilling:\n- community.shopify.dev/t/unable-to-approve-billing-api-test-charges-on-plus-development-stores/23258\n- community.shopify.com/c/technical-q-a/apps-without-a-public-distribution-cannot-use-the-billing-api/m-p/1757459\n- community.shopify.dev/t/possible-bug-negative-duration-billing-cycle-for-subscription/25346\n\nSession tokens / iframe:\n- community.shopify.com/c/shopify-apps/managing-embedded-app-user-session-lost/td-p/1038381\n- shopify.dev/docs/apps/build/authentication-authorization/session-tokens/set-up-session-tokens\n\nApp Review:\n- community.shopify.dev/t/warning-shopify-app-store-review-process/32259\n- community.shopify.dev/t/frustration-with-app-review-process/31784\n- community.shopify.dev/t/application-review-rejected/17950\n- shopify.dev/docs/apps/store/common-rejections\n"
}

SHA-256 of public snapshot: e707b7585ceff2eeb1f1cbdb393d7829a2213fbf51d940d6d350836dc29cab3f