[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-f0af735f-f42f-43ee-afd8-f3089278c17e":3,"$fSj-XRF_g0v-g_qHy6EmVWrBEQbRVr-UBRXttOAY3tF0":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"f0af735f-f42f-43ee-afd8-f3089278c17e","shopify-apps","Shopify应用开发的专业模式，包括Remix\u002FReact","cat_coding_frontend","mod_coding","sickn33,coding","---\nname: shopify-apps\ndescription: Expert patterns for Shopify app development including Remix\u002FReact\n  Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin\n  API, Polaris components, billing, and app extensions.\nrisk: safe\nsource: vibeship-spawner-skills (Apache 2.0)\ndate_added: 2026-02-27\n---\n\n# Shopify Apps\n\nExpert patterns for Shopify app development including Remix\u002FReact Router apps,\nembedded apps with App Bridge, webhook handling, GraphQL Admin API,\nPolaris components, billing, and app extensions.\n\n## Patterns\n\n### React Router App Setup\n\nModern Shopify app template with React Router\n\n**When to use**: Starting a new Shopify app\n\n### Template\n\n# Create new Shopify app with CLI\nnpm init @shopify\u002Fapp@latest my-shopify-app\n\n# Project structure\n# my-shopify-app\u002F\n# ├── app\u002F\n# │   ├── routes\u002F\n# │   │   ├── app._index.tsx        # Main app page\n# │   │   ├── app.tsx               # App layout with providers\n# │   │   ├── auth.$.tsx            # Auth callback\n# │   │   └── webhooks.tsx          # Webhook handler\n# │   ├── shopify.server.ts         # Server configuration\n# │   └── root.tsx                  # Root layout\n# ├── extensions\u002F                   # App extensions\n# ├── shopify.app.toml              # App configuration\n# └── package.json\n\n\u002F\u002F shopify.app.toml\nname = \"my-shopify-app\"\nclient_id = \"your-client-id\"\napplication_url = \"https:\u002F\u002Fyour-app.example.com\"\n\n[access_scopes]\nscopes = \"read_products,write_products,read_orders\"\n\n[webhooks]\napi_version = \"2024-10\"\n\n[webhooks.subscriptions]\ntopics = [\"orders\u002Fcreate\", \"products\u002Fupdate\"]\nuri = \"\u002Fwebhooks\"\n\n[auth]\nredirect_urls = [\"https:\u002F\u002Fyour-app.example.com\u002Fauth\u002Fcallback\"]\n\n\u002F\u002F app\u002Fshopify.server.ts\nimport \"@shopify\u002Fshopify-app-remix\u002Fadapters\u002Fnode\";\nimport {\n  LATEST_API_VERSION,\n  shopifyApp,\n  DeliveryMethod,\n} from \"@shopify\u002Fshopify-app-remix\u002Fserver\";\nimport { PrismaSessionStorage } from \"@shopify\u002Fshopify-app-session-storage-prisma\";\nimport prisma from \".\u002Fdb.server\";\n\nconst shopify = shopifyApp({\n  apiKey: process.env.SHOPIFY_API_KEY!,\n  apiSecretKey: process.env.SHOPIFY_API_SECRET!,\n  scopes: process.env.SCOPES?.split(\",\"),\n  appUrl: process.env.SHOPIFY_APP_URL!,\n  authPathPrefix: \"\u002Fauth\",\n  sessionStorage: new PrismaSessionStorage(prisma),\n  distribution: AppDistribution.AppStore,\n  future: {\n    unstable_newEmbeddedAuthStrategy: true,\n  },\n  ...(process.env.SHOP_CUSTOM_DOMAIN\n    ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }\n    : {}),\n});\n\nexport default shopify;\nexport const apiVersion = LATEST_API_VERSION;\nexport const authenticate = shopify.authenticate;\nexport const sessionStorage = shopify.sessionStorage;\n\n### Notes\n\n- React Router replaced Remix as recommended template (late 2024)\n- unstable_newEmbeddedAuthStrategy enabled by default for new apps\n- Webhooks configured in shopify.app.toml, not code\n- Run 'shopify app deploy' to apply configuration changes\n\n### Embedded App with App Bridge\n\nRender app embedded in Shopify Admin\n\n**When to use**: Building embedded admin app\n\n### Template\n\n\u002F\u002F app\u002Froutes\u002Fapp.tsx - App layout with providers\nimport { Link, Outlet, useLoaderData, useRouteError } from \"@remix-run\u002Freact\";\nimport { AppProvider } from \"@shopify\u002Fshopify-app-remix\u002Freact\";\nimport polarisStyles from \"@shopify\u002Fpolaris\u002Fbuild\u002Fesm\u002Fstyles.css?url\";\n\nexport const links = () => [{ rel: \"stylesheet\", href: polarisStyles }];\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n  await authenticate.admin(request);\n  return json({ apiKey: process.env.SHOPIFY_API_KEY! });\n}\n\nexport default function App() {\n  const { apiKey } = useLoaderData\u003Ctypeof loader>();\n\n  return (\n    \u003CAppProvider isEmbeddedApp apiKey={apiKey}>\n      \u003Cui-nav-menu>\n        \u003CLink to=\"\u002Fapp\" rel=\"home\">Home\u003C\u002FLink>\n        \u003CLink to=\"\u002Fapp\u002Fproducts\">Products\u003C\u002FLink>\n        \u003CLink to=\"\u002Fapp\u002Fsettings\">Settings\u003C\u002FLink>\n      \u003C\u002Fui-nav-menu>\n      \u003COutlet \u002F>\n    \u003C\u002FAppProvider>\n  );\n}\n\nexport function ErrorBoundary() {\n  const error = useRouteError();\n  return (\n    \u003CAppProvider isEmbeddedApp>\n      \u003CPage>\n        \u003CCard>\n          \u003CText as=\"p\" variant=\"bodyMd\">\n            Something went wrong. Please try again.\n          \u003C\u002FText>\n        \u003C\u002FCard>\n      \u003C\u002FPage>\n    \u003C\u002FAppProvider>\n  );\n}\n\n\u002F\u002F app\u002Froutes\u002Fapp._index.tsx - Main app page\nimport {\n  Page,\n  Layout,\n  Card,\n  Text,\n  BlockStack,\n  Button,\n} from \"@shopify\u002Fpolaris\";\nimport { TitleBar } from \"@shopify\u002Fapp-bridge-react\";\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n  const { admin } = await authenticate.admin(request);\n\n  \u002F\u002F GraphQL query\n  const response = await admin.graphql(`\n    query {\n      shop {\n        name\n        email\n      }\n    }\n  `);\n\n  const { data } = await response.json();\n  return json({ shop: data.shop });\n}\n\nexport default function Index() {\n  const { shop } = useLoaderData\u003Ctypeof loader>();\n\n  return (\n    \u003CPage>\n      \u003CTitleBar title=\"My Shopify App\" \u002F>\n      \u003CLayout>\n        \u003CLayout.Section>\n          \u003CCard>\n            \u003CBlockStack gap=\"200\">\n              \u003CText as=\"h2\" variant=\"headingMd\">\n                Welcome to {shop.name}!\n              \u003C\u002FText>\n              \u003CText as=\"p\" variant=\"bodyMd\">\n                Your app is now connected to this store.\n              \u003C\u002FText>\n              \u003CButton variant=\"primary\">\n                Get Started\n              \u003C\u002FButton>\n            \u003C\u002FBlockStack>\n          \u003C\u002FCard>\n        \u003C\u002FLayout.Section>\n      \u003C\u002FLayout>\n    \u003C\u002FPage>\n  );\n}\n\n### Notes\n\n- App Bridge required for Built for Shopify (July 2025)\n- Polaris components match Shopify Admin design\n- TitleBar and navigation from App Bridge\n- Always authenticate requests with authenticate.admin()\n\n### Webhook Handling\n\nSecure webhook processing with HMAC verification\n\n**When to use**: Receiving Shopify webhooks\n\n### Template\n\n\u002F\u002F app\u002Froutes\u002Fwebhooks.tsx\nimport type { ActionFunctionArgs } from \"@remix-run\u002Fnode\";\nimport { authenticate } from \"..\u002Fshopify.server\";\nimport db from \"..\u002Fdb.server\";\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  \u002F\u002F Authenticate webhook (verifies HMAC signature)\n  const { topic, shop, payload, admin } = await authenticate.webhook(request);\n\n  console.log(`Received ${topic} webhook for ${shop}`);\n\n  \u002F\u002F Process based on topic\n  switch (topic) {\n    case \"ORDERS_CREATE\":\n      \u002F\u002F Queue for async processing\n      await queueOrderProcessing(payload);\n      break;\n\n    case \"PRODUCTS_UPDATE\":\n      await handleProductUpdate(shop, payload);\n      break;\n\n    case \"APP_UNINSTALLED\":\n      \u002F\u002F Clean up shop data\n      await db.session.deleteMany({ where: { shop } });\n      await db.shopData.delete({ where: { shop } });\n      break;\n\n    case \"CUSTOMERS_DATA_REQUEST\":\n    case \"CUSTOMERS_REDACT\":\n    case \"SHOP_REDACT\":\n      \u002F\u002F GDPR webhooks - mandatory\n      await handleGDPRWebhook(topic, payload);\n      break;\n\n    default:\n      console.log(`Unhandled webhook topic: ${topic}`);\n  }\n\n  \u002F\u002F CRITICAL: Return 200 immediately\n  \u002F\u002F Shopify expects response within 5 seconds\n  return new Response(null, { status: 200 });\n};\n\n\u002F\u002F Process asynchronously after responding\nasync function queueOrderProcessing(payload: any) {\n  \u002F\u002F Use a job queue (BullMQ, etc.)\n  await jobQueue.add(\"process-order\", {\n    orderId: payload.id,\n    orderData: payload,\n  });\n}\n\nasync function handleProductUpdate(shop: string, payload: any) {\n  \u002F\u002F Quick sync operation only\n  await db.product.upsert({\n    where: { shopifyId: payload.id },\n    update: {\n      title: payload.title,\n      updatedAt: new Date(),\n    },\n    create: {\n      shopifyId: payload.id,\n      shop,\n      title: payload.title,\n    },\n  });\n}\n\nasync function handleGDPRWebhook(topic: string, payload: any) {\n  \u002F\u002F GDPR compliance - required for all apps\n  switch (topic) {\n    case \"CUSTOMERS_DATA_REQUEST\":\n      \u002F\u002F Return customer data within 30 days\n      break;\n    case \"CUSTOMERS_REDACT\":\n      \u002F\u002F Delete customer data\n      break;\n    case \"SHOP_REDACT\":\n      \u002F\u002F Delete all shop data (48 hours after uninstall)\n      break;\n  }\n}\n\n### Notes\n\n- Respond within 5 seconds or webhook fails\n- Use job queues for heavy processing\n- GDPR webhooks are mandatory for App Store\n- HMAC verification handled by authenticate.webhook()\n\n### GraphQL Admin API\n\nQuery and mutate shop data with GraphQL\n\n**When to use**: Interacting with Shopify Admin API\n\n### Template\n\n\u002F\u002F GraphQL queries with authenticated admin client\nexport async function loader({ request }: LoaderFunctionArgs) {\n  const { admin } = await authenticate.admin(request);\n\n  \u002F\u002F Query products with pagination\n  const response = await admin.graphql(`\n    query GetProducts($first: Int!, $after: String) {\n      products(first: $first, after: $after) {\n        edges {\n          node {\n            id\n            title\n            status\n            totalInventory\n            priceRangeV2 {\n              minVariantPrice {\n                amount\n                currencyCode\n              }\n            }\n            images(first: 1) {\n              edges {\n                node {\n                  url\n                  altText\n                }\n              }\n            }\n          }\n          cursor\n        }\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n      }\n    }\n  `, {\n    variables: {\n      first: 10,\n      after: null,\n    },\n  });\n\n  const { data } = await response.json();\n  return json({ products: data.products });\n}\n\n\u002F\u002F Mutations\nexport async function action({ request }: ActionFunctionArgs) {\n  const { admin } = await authenticate.admin(request);\n  const formData = await request.formData();\n  const productId = formData.get(\"productId\");\n  const newTitle = formData.get(\"title\");\n\n  const response = await admin.graphql(`\n    mutation UpdateProduct($input: ProductInput!) {\n      productUpdate(input: $input) {\n        product {\n          id\n          title\n        }\n        userErrors {\n          field\n          message\n        }\n      }\n    }\n  `, {\n    variables: {\n      input: {\n        id: productId,\n        title: newTitle,\n      },\n    },\n  });\n\n  const { data } = await response.json();\n\n  if (data.productUpdate.userErrors.length > 0) {\n    return json({\n      errors: data.productUpdate.userErrors,\n    }, { status: 400 });\n  }\n\n  return json({ product: data.productUpdate.product });\n}\n\n\u002F\u002F Bulk operations for large datasets\nasync function bulkUpdateProducts(admin: AdminApiContext) {\n  \u002F\u002F Create bulk operation\n  const response = await admin.graphql(`\n    mutation {\n      bulkOperationRunMutation(\n        mutation: \"mutation call($input: ProductInput!) {\n          productUpdate(input: $input) { product { id } }\n        }\",\n        stagedUploadPath: \"path-to-staged-upload\"\n      ) {\n        bulkOperation {\n          id\n          status\n        }\n        userErrors {\n          message\n        }\n      }\n    }\n  `);\n\n  \u002F\u002F Poll for completion or use webhook\n  \u002F\u002F BULK_OPERATIONS_FINISH webhook\n}\n\n### Notes\n\n- GraphQL required for new public apps (April 2025)\n- Rate limit: 1000 points per 60 seconds\n- Use bulk operations for >250 items\n- Direct API access available from App Bridge\n\n### Billing API Integration\n\nImplement subscription billing for your app\n\n**When to use**: Monetizing Shopify app\n\n### Template\n\n\u002F\u002F app\u002Froutes\u002Fapp.billing.tsx\nimport { json, redirect } from \"@remix-run\u002Fnode\";\nimport { Page, Card, Button, BlockStack, Text } from \"@shopify\u002Fpolaris\";\nimport { authenticate } from \"..\u002Fshopify.server\";\n\nconst PLANS = {\n  basic: {\n    name: \"Basic\",\n    amount: 9.99,\n    currencyCode: \"USD\",\n    interval: \"EVERY_30_DAYS\",\n  },\n  pro: {\n    name: \"Pro\",\n    amount: 29.99,\n    currencyCode: \"USD\",\n    interval: \"EVERY_30_DAYS\",\n  },\n};\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n  const { admin, billing } = await authenticate.admin(request);\n\n  \u002F\u002F Check current subscription\n  const response = await admin.graphql(`\n    query {\n      currentAppInstallation {\n        activeSubscriptions {\n          id\n          name\n          status\n          lineItems {\n            plan {\n              pricingDetails {\n                ... on AppRecurringPricing {\n                  price {\n                    amount\n                    currencyCode\n                  }\n                  interval\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `);\n\n  const { data } = await response.json();\n  return json({\n    subscription: data.currentAppInstallation.activeSubscriptions[0],\n  });\n}\n\nexport async function action({ request }: ActionFunctionArgs) {\n  const { admin, session } = await authenticate.admin(request);\n  const formData = await request.formData();\n  const planKey = formData.get(\"plan\") as keyof typeof PLANS;\n  const plan = PLANS[planKey];\n\n  \u002F\u002F Create subscription charge\n  const response = await admin.graphql(`\n    mutation CreateSubscription($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $test: Boolean) {\n      appSubscriptionCreate(\n        name: $name\n        lineItems: $lineItems\n        returnUrl: $returnUrl\n        test: $test\n      ) {\n        appSubscription {\n          id\n          status\n        }\n        confirmationUrl\n        userErrors {\n          field\n          message\n        }\n      }\n    }\n  `, {\n    variables: {\n      name: plan.name,\n      lineItems: [\n        {\n          plan: {\n            appRecurringPricingDetails: {\n              price: {\n                amount: plan.amount,\n                currencyCode: plan.currencyCode,\n              },\n              interval: plan.interval,\n            },\n          },\n        },\n      ],\n      returnUrl: `https:\u002F\u002F${session.shop}\u002Fadmin\u002Fapps\u002F${process.env.SHOPIFY_API_KEY}`,\n      test: process.env.NODE_ENV !== \"production\",\n    },\n  });\n\n  const { data } = await response.json();\n\n  if (data.appSubscriptionCreate.userErrors.length > 0) {\n    return json({\n      errors: data.appSubscriptionCreate.userErrors,\n    }, { status: 400 });\n  }\n\n  \u002F\u002F Redirect merchant to approve charge\n  return redirect(data.appSubscriptionCreate.confirmationUrl);\n}\n\nexport default function Billing() {\n  const { subscription } = useLoaderData\u003Ctypeof loader>();\n  const submit = useSubmit();\n\n  return (\n    \u003CPage title=\"Billing\">\n      \u003CCard>\n        {subscription ? (\n          \u003CBlockStack gap=\"200\">\n            \u003CText as=\"p\" variant=\"bodyMd\">\n              Current plan: {subscription.name}\n            \u003C\u002FText>\n            \u003CText as=\"p\" variant=\"bodyMd\">\n              Status: {subscription.status}\n            \u003C\u002FText>\n          \u003C\u002FBlockStack>\n        ) : (\n          \u003CBlockStack gap=\"400\">\n            \u003CText as=\"h2\" variant=\"headingMd\">\n              Choose a Plan\n            \u003C\u002FText>\n            \u003CButton onClick={() => submit({ plan: \"basic\" }, { method: \"post\" })}>\n              Basic - $9.99\u002Fmonth\n            \u003C\u002FButton>\n            \u003CButton onClick={() => submit({ plan: \"pro\" }, { method: \"post\" })}>\n              Pro - $29.99\u002Fmonth\n            \u003C\u002FButton>\n          \u003C\u002FBlockStack>\n        )}\n      \u003C\u002FCard>\n    \u003C\u002FPage>\n  );\n}\n\n### Notes\n\n- Use test: true for development stores\n- Merchant must approve subscription\n- One recurring + one usage charge per app max\n- 30-day billing cycle for recurring charges\n\n### App Extension Development\n\nExtend Shopify checkout, admin, or storefront\n\n**When to use**: Building app extensions\n\n### Template\n\n# shopify.extension.toml (in extensions\u002Fmy-extension\u002F)\napi_version = \"2024-10\"\n\n[[extensions]]\ntype = \"ui_extension\"\nname = \"Product Customizer\"\nhandle = \"product-customizer\"\n\n[[extensions.targeting]]\ntarget = \"admin.product-details.block.render\"\nmodule = \".\u002Fsrc\u002FAdminBlock.tsx\"\n\n[extensions.capabilities]\napi_access = true\n\n[extensions.settings]\n[[extensions.settings.fields]]\nkey = \"show_preview\"\ntype = \"boolean\"\nname = \"Show Preview\"\n\n\u002F\u002F extensions\u002Fmy-extension\u002Fsrc\u002FAdminBlock.tsx\nimport {\n  reactExtension,\n  useApi,\n  useSettings,\n  BlockStack,\n  Text,\n  Button,\n  InlineStack,\n} from \"@shopify\u002Fui-extensions-react\u002Fadmin\";\n\nexport default reactExtension(\n  \"admin.product-details.block.render\",\n  () => \u003CProductCustomizer \u002F>\n);\n\nfunction ProductCustomizer() {\n  const { data, extension } = useApi\u003C\"admin.product-details.block.render\">();\n  const settings = useSettings();\n\n  const productId = data?.selected?.[0]?.id;\n\n  const handleCustomize = async () => {\n    \u002F\u002F API calls from extension\n    const result = await fetch(\"\u002Fapi\u002Fcustomize\", {\n      method: \"POST\",\n      body: JSON.stringify({ productId }),\n    });\n  };\n\n  return (\n    \u003CBlockStack gap=\"base\">\n      \u003CText fontWeight=\"bold\">Product Customizer\u003C\u002FText>\n      \u003CText>\n        Customize product: {productId}\n      \u003C\u002FText>\n      {settings.show_preview && (\n        \u003CText size=\"small\">Preview enabled\u003C\u002FText>\n      )}\n      \u003CInlineStack gap=\"base\">\n        \u003CButton onPress={handleCustomize}>\n          Apply Customization\n        \u003C\u002FButton>\n      \u003C\u002FInlineStack>\n    \u003C\u002FBlockStack>\n  );\n}\n\n\u002F\u002F Checkout UI Extension\n\u002F\u002F [[extensions.targeting]]\n\u002F\u002F target = \"purchase.checkout.block.render\"\n\n\u002F\u002F extensions\u002Fcheckout-ext\u002Fsrc\u002FCheckout.tsx\nimport {\n  reactExtension,\n  Banner,\n  useCartLines,\n  useTotalAmount,\n} from \"@shopify\u002Fui-extensions-react\u002Fcheckout\";\n\nexport default reactExtension(\n  \"purchase.checkout.block.render\",\n  () => \u003CCheckoutBanner \u002F>\n);\n\nfunction CheckoutBanner() {\n  const cartLines = useCartLines();\n  const total = useTotalAmount();\n\n  if (total.amount > 100) {\n    return (\n      \u003CBanner status=\"success\">\n        You qualify for free shipping!\n      \u003C\u002FBanner>\n    );\n  }\n\n  return null;\n}\n\n### Notes\n\n- Extensions run in sandboxed iframe\n- Use @shopify\u002Fui-extensions-react for React\n- Limited APIs compared to full app\n- Deploy with 'shopify app deploy'\n\n## Sharp Edges\n\n### Webhook Must Respond Within 5 Seconds\n\nSeverity: HIGH\n\nSituation: Receiving webhooks from Shopify\n\nSymptoms:\nWebhook deliveries marked as failed.\n\"Your app didn't respond in time\" in Shopify logs.\nMissing order\u002Fproduct updates.\nWebhooks retried repeatedly then cancelled.\n\nWhy this breaks:\nShopify expects a 2xx response within 5 seconds. If your app processes\nthe webhook data before responding, you'll timeout.\n\nShopify retries failed webhooks up to 19 times over 48 hours.\nAfter continued failures, webhooks may be cancelled entirely.\n\nHeavy processing (API calls, database operations) must happen\nafter the response is sent.\n\nRecommended fix:\n\n## Respond immediately, process asynchronously\n\n```typescript\n\u002F\u002F app\u002Froutes\u002Fwebhooks.tsx\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  const { topic, shop, payload } = await authenticate.webhook(request);\n\n  \u002F\u002F Queue for async processing\n  await jobQueue.add(\"process-webhook\", {\n    topic,\n    shop,\n    payload,\n  });\n\n  \u002F\u002F CRITICAL: Return 200 immediately\n  return new Response(null, { status: 200 });\n};\n\n\u002F\u002F Worker process handles the actual work\n\u002F\u002F workers\u002Fwebhook-processor.ts\nimport { Worker } from \"bullmq\";\n\nconst worker = new Worker(\"process-webhook\", async (job) => {\n  const { topic, shop, payload } = job.data;\n\n  switch (topic) {\n    case \"ORDERS_CREATE\":\n      await processOrder(shop, payload);\n      break;\n    \u002F\u002F ... other handlers\n  }\n});\n```\n\n## For simple operations, be quick\n\n```typescript\n\u002F\u002F Simple database update is OK if fast\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  const { topic, payload } = await authenticate.webhook(request);\n\n  \u002F\u002F Quick database update (\u003C 1 second)\n  await db.product.update({\n    where: { shopifyId: payload.id },\n    data: { title: payload.title },\n  });\n\n  return new Response(null, { status: 200 });\n};\n```\n\n## Monitor webhook performance\n\n```typescript\n\u002F\u002F Log response times\nconst start = Date.now();\n\nawait handleWebhook(payload);\n\nconst duration = Date.now() - start;\nconsole.log(`Webhook processed in ${duration}ms`);\n\n\u002F\u002F Alert if approaching timeout\nif (duration > 3000) {\n  console.warn(\"Webhook processing taking too long!\");\n}\n```\n\n### API Rate Limits Cause 429 Errors\n\nSeverity: HIGH\n\nSituation: Making API calls to Shopify\n\nSymptoms:\nHTTP 429 Too Many Requests errors.\n\"Throttled\" responses.\nApp becomes unresponsive.\nOperations fail silently or partially.\n\nWhy this breaks:\nShopify enforces strict rate limits:\n- REST: 2 requests per second per store\n- GraphQL: 1000 points per 60 seconds\n\nExceeding limits causes immediate 429 errors.\nContinuous violations can result in temporary bans.\n\nBulk operations count against limits.\n\nRecommended fix:\n\n## Check rate limit headers\n\n```typescript\n\u002F\u002F REST API\n\u002F\u002F X-Shopify-Shop-Api-Call-Limit: 39\u002F40\n\n\u002F\u002F GraphQL - check response extensions\nconst response = await admin.graphql(`...`);\nconst { data, extensions } = await response.json();\n\nconst cost = extensions?.cost;\n\u002F\u002F {\n\u002F\u002F   \"requestedQueryCost\": 42,\n\u002F\u002F   \"actualQueryCost\": 42,\n\u002F\u002F   \"throttleStatus\": {\n\u002F\u002F     \"maximumAvailable\": 1000,\n\u002F\u002F     \"currentlyAvailable\": 958,\n\u002F\u002F     \"restoreRate\": 50\n\u002F\u002F   }\n\u002F\u002F }\n```\n\n## Implement retry with exponential backoff\n\n```typescript\nasync function shopifyRequest(\n  fn: () => Promise\u003CResponse>,\n  maxRetries = 3\n): Promise\u003CResponse> {\n  let lastError: Error;\n\n  for (let attempt = 0; attempt \u003C maxRetries; attempt++) {\n    try {\n      const response = await fn();\n\n      if (response.status === 429) {\n        \u002F\u002F Get retry-after header or default\n        const retryAfter = parseInt(\n          response.headers.get(\"Retry-After\") || \"2\"\n        );\n        await sleep(retryAfter * 1000 * Math.pow(2, attempt));\n        continue;\n      }\n\n      return response;\n    } catch (error) {\n      lastError = error as Error;\n    }\n  }\n\n  throw lastError!;\n}\n```\n\n## Use bulk operations for large datasets\n\n```typescript\n\u002F\u002F Instead of 1000 individual calls, use bulk mutation\nconst response = await admin.graphql(`\n  mutation {\n    bulkOperationRunMutation(\n      mutation: \"mutation($input: ProductInput!) {\n        productUpdate(input: $input) { product { id } }\n      }\",\n      stagedUploadPath: \"...\"\n    ) {\n      bulkOperation { id status }\n      userErrors { message }\n    }\n  }\n`);\n```\n\n## Queue requests\n\n```typescript\nimport { RateLimiter } from \"limiter\";\n\n\u002F\u002F 2 requests per second for REST\nconst limiter = new RateLimiter({\n  tokensPerInterval: 2,\n  interval: \"second\",\n});\n\nasync function rateLimitedRequest(fn: () => Promise\u003Cany>) {\n  await limiter.removeTokens(1);\n  return fn();\n}\n```\n\n### Protected Customer Data Requires Special Permission\n\nSeverity: HIGH\n\nSituation: Accessing customer PII in webhooks or API\n\nSymptoms:\nWebhook deliveries fail for orders\u002Fcustomers.\nCustomer data fields are null or empty.\nApp works in development but fails in production.\n\"Protected customer data access\" errors.\n\nWhy this breaks:\nSince April 2024, accessing protected customer data (PII) requires\nexplicit approval from Shopify. This is separate from OAuth scopes.\n\nProtected data includes:\n- Customer names, emails, addresses\n- Order customer information\n- Subscription customer details\n\nEven with read_orders scope, you won't receive customer data\nin webhooks without protected data access.\n\nRecommended fix:\n\n## Request protected customer data access\n\n1. Go to Partner Dashboard > App > API access\n2. Under \"Protected customer data access\"\n3. Request access for needed data types\n4. Justify your use case\n5. Wait for Shopify approval (can take days)\n\n## Check your data access level\n\n```typescript\n\u002F\u002F Query your app's data access\nconst response = await admin.graphql(`\n  query {\n    currentAppInstallation {\n      accessScopes {\n        handle\n      }\n    }\n  }\n`);\n```\n\n## Handle missing data gracefully\n\n```typescript\n\u002F\u002F Webhook payload may have redacted fields\nasync function processOrder(payload: any) {\n  const customerEmail = payload.customer?.email;\n\n  if (!customerEmail) {\n    \u002F\u002F Customer data not available\n    \u002F\u002F Either no protected access or data redacted\n    console.log(\"Customer data not available\");\n    return;\n  }\n\n  await sendOrderConfirmation(customerEmail);\n}\n```\n\n## Use customer account API for direct access\n\n```typescript\n\u002F\u002F If customer is logged in, can access their data\n\u002F\u002F through Customer Account API (different from Admin API)\n```\n\n### Duplicate Webhook Definitions Cause Conflicts\n\nSeverity: MEDIUM\n\nSituation: Configuring webhooks in both TOML and code\n\nSymptoms:\nDuplicate webhook deliveries.\nSome webhooks fire twice.\nWebhook subscriptions fail to register.\nUnpredictable webhook behavior.\n\nWhy this breaks:\nShopify apps can define webhooks in two places:\n1. shopify.app.toml (declarative, recommended)\n2. afterAuth hook in code (imperative, legacy)\n\nIf you define the same webhook in both places, you get:\n- Duplicate subscriptions\n- Race conditions during registration\n- Conflicts during app updates\n\nRecommended fix:\n\n## Use TOML only (recommended)\n\n```toml\n# shopify.app.toml\n[webhooks]\napi_version = \"2024-10\"\n\n[webhooks.subscriptions]\ntopics = [\n  \"orders\u002Fcreate\",\n  \"orders\u002Fupdated\",\n  \"products\u002Fcreate\",\n  \"products\u002Fupdate\",\n  \"app\u002Funinstalled\"\n]\nuri = \"\u002Fwebhooks\"\n```\n\n## Remove code-based registration\n\n```typescript\n\u002F\u002F DON'T do this if using TOML\nconst shopify = shopifyApp({\n  \u002F\u002F ...\n  hooks: {\n    afterAuth: async ({ session }) => {\n      \u002F\u002F Remove webhook registration from here\n      \u002F\u002F Let TOML handle it\n    },\n  },\n});\n```\n\n## Deploy to apply TOML changes\n\n```bash\n# Webhooks registered on deploy\nshopify app deploy\n```\n\n## Check current subscriptions\n\n```typescript\nconst response = await admin.graphql(`\n  query {\n    webhookSubscriptions(first: 50) {\n      edges {\n        node {\n          id\n          topic\n          endpoint {\n            ... on WebhookHttpEndpoint {\n              callbackUrl\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n```\n\n### Webhook URL Trailing Slash Causes 404\n\nSeverity: MEDIUM\n\nSituation: Setting up webhook endpoints\n\nSymptoms:\nWebhooks return 404 Not Found.\nWebhook delivery fails immediately.\nWorks in local dev but fails in production.\nLogs show request to \u002Fwebhooks\u002F not \u002Fwebhooks.\n\nWhy this breaks:\nShopify automatically adds a trailing slash to webhook URLs.\nIf your server doesn't handle both \u002Fwebhooks and \u002Fwebhooks\u002F,\nthe webhook will 404.\n\nCommon with frameworks that are strict about trailing slashes.\n\nRecommended fix:\n\n## Handle both URL formats\n\n```typescript\n\u002F\u002F Remix\u002FReact Router - both work by default\n\u002F\u002F app\u002Froutes\u002Fwebhooks.tsx handles \u002Fwebhooks\n\n\u002F\u002F Express - add middleware\napp.use((req, res, next) => {\n  if (req.path.endsWith('\u002F') && req.path.length > 1) {\n    const query = req.url.slice(req.path.length);\n    const safePath = req.path.slice(0, -1);\n    res.redirect(301, safePath + query);\n  }\n  next();\n});\n```\n\n## Configure web server\n\n```nginx\n# Nginx - strip trailing slashes\nlocation ~ ^(.+)\u002F$ {\n  return 301 $1;\n}\n\n# Or rewrite to handler\nlocation \u002Fwebhooks {\n  try_files $uri $uri\u002F @webhooks;\n}\nlocation @webhooks {\n  proxy_pass http:\u002F\u002Fapp:3000\u002Fwebhooks;\n}\n```\n\n## Test both formats\n\n```bash\n# Test without slash\ncurl -X POST https:\u002F\u002Fyour-app.com\u002Fwebhooks\n\n# Test with slash\ncurl -X POST https:\u002F\u002Fyour-app.com\u002Fwebhooks\u002F\n```\n\n### REST API Required Migration to GraphQL (April 2025)\n\nSeverity: HIGH\n\nSituation: Building new public apps or maintaining existing\n\nSymptoms:\nApp store submission rejected for REST API usage.\nDeprecation warnings in console.\nSome REST endpoints stop working.\nMissing features only in GraphQL.\n\nWhy this breaks:\nAs of October 2024, REST Admin API is legacy.\nStarting April 2025, new public apps MUST use GraphQL.\n\nREST endpoints will continue working for existing apps,\nbut new features are GraphQL-only.\n\nMetafields, bulk operations, and many new features\nrequire GraphQL.\n\nRecommended fix:\n\n## Use GraphQL for all new code\n\n```typescript\n\u002F\u002F REST (legacy)\nconst response = await fetch(\n  `https:\u002F\u002F${shop}\u002Fadmin\u002Fapi\u002F2024-10\u002Fproducts.json`,\n  {\n    headers: { \"X-Shopify-Access-Token\": token },\n  }\n);\n\n\u002F\u002F GraphQL (recommended)\nconst response = await admin.graphql(`\n  query {\n    products(first: 10) {\n      edges {\n        node {\n          id\n          title\n        }\n      }\n    }\n  }\n`);\n```\n\n## Migrate existing REST calls\n\n```typescript\n\u002F\u002F REST: GET \u002Fproducts\u002F{id}.json\n\u002F\u002F GraphQL equivalent:\nconst response = await admin.graphql(`\n  query GetProduct($id: ID!) {\n    product(id: $id) {\n      id\n      title\n      status\n      variants(first: 10) {\n        edges {\n          node {\n            id\n            price\n            inventoryQuantity\n          }\n        }\n      }\n    }\n  }\n`, {\n  variables: { id: `gid:\u002F\u002Fshopify\u002FProduct\u002F${productId}` },\n});\n```\n\n## Use GraphQL for webhooks too\n\n```toml\n# shopify.app.toml\n[webhooks]\napi_version = \"2024-10\"  # Use latest GraphQL version\n```\n\n### App Bridge Required for Built for Shopify (July 2025)\n\nSeverity: HIGH\n\nSituation: Building embedded Shopify apps\n\nSymptoms:\nApp rejected from \"Built for Shopify\" program.\nApp not appearing correctly in admin.\nNavigation and chrome issues.\nWarning about App Bridge version.\n\nWhy this breaks:\nEffective July 2025, all apps seeking \"Built for Shopify\" status\nmust use the latest version of App Bridge and be embedded.\n\nApps using old App Bridge versions or not embedded will\nlose built for Shopify benefits (better placement, badges).\n\nShopify now serves App Bridge and Polaris via unversioned\nscript tags that auto-update.\n\nRecommended fix:\n\n## Use latest App Bridge via script tag\n\n```html\n\u003C!-- Automatically stays up to date -->\n\u003Cscript src=\"https:\u002F\u002Fcdn.shopify.com\u002Fshopifycloud\u002Fapp-bridge.js\">\u003C\u002Fscript>\n```\n\n## Use AppProvider in React\n\n```typescript\n\u002F\u002F app\u002Froutes\u002Fapp.tsx\nimport { AppProvider } from \"@shopify\u002Fshopify-app-remix\u002Freact\";\n\nexport default function App() {\n  return (\n    \u003CAppProvider isEmbeddedApp apiKey={apiKey}>\n      \u003COutlet \u002F>\n    \u003C\u002FAppProvider>\n  );\n}\n```\n\n## Enable embedded auth strategy\n\n```typescript\n\u002F\u002F shopify.server.ts\nconst shopify = shopifyApp({\n  \u002F\u002F ...\n  future: {\n    unstable_newEmbeddedAuthStrategy: true,\n  },\n});\n```\n\n## Check embedded status\n\n```typescript\nimport { useAppBridge } from \"@shopify\u002Fapp-bridge-react\";\n\nfunction MyComponent() {\n  const app = useAppBridge();\n  const isEmbedded = app.hostOrigin !== window.location.origin;\n}\n```\n\n### Missing GDPR Webhooks Block App Store Approval\n\nSeverity: HIGH\n\nSituation: Submitting app to Shopify App Store\n\nSymptoms:\nApp submission rejected.\n\"GDPR webhooks not implemented\" error.\nManual review fails for compliance.\nData request webhooks not handled.\n\nWhy this breaks:\nShopify requires all apps to handle three GDPR webhooks:\n1. customers\u002Fdata_request - Provide customer data\n2. customers\u002Fredact - Delete customer data\n3. shop\u002Fredact - Delete all shop data\n\nThese are automatically subscribed when you create an app.\nYou MUST implement handlers even if you don't store data.\n\nRecommended fix:\n\n## Implement all GDPR handlers\n\n```typescript\n\u002F\u002F app\u002Froutes\u002Fwebhooks.tsx\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  const { topic, payload, shop } = await authenticate.webhook(request);\n\n  switch (topic) {\n    case \"CUSTOMERS_DATA_REQUEST\":\n      await handleDataRequest(shop, payload);\n      break;\n\n    case \"CUSTOMERS_REDACT\":\n      await handleCustomerRedact(shop, payload);\n      break;\n\n    case \"SHOP_REDACT\":\n      await handleShopRedact(shop, payload);\n      break;\n  }\n\n  return new Response(null, { status: 200 });\n};\n\nasync function handleDataRequest(shop: string, payload: any) {\n  const customerId = payload.customer.id;\n\n  \u002F\u002F Return customer data within 30 days\n  \u002F\u002F Usually send to data_request.destination_url\n  const customerData = await db.customer.findUnique({\n    where: { shopifyId: customerId, shop },\n  });\n\n  if (customerData) {\n    \u002F\u002F Send to provided URL or email\n    await sendDataToMerchant(payload.data_request, customerData);\n  }\n}\n\nasync function handleCustomerRedact(shop: string, payload: any) {\n  const customerId = payload.customer.id;\n\n  \u002F\u002F Delete customer's personal data\n  await db.customer.deleteMany({\n    where: { shopifyId: customerId, shop },\n  });\n\n  await db.order.updateMany({\n    where: { customerId, shop },\n    data: { customerEmail: null, customerName: null },\n  });\n}\n\nasync function handleShopRedact(shop: string, payload: any) {\n  \u002F\u002F Shop uninstalled 48+ hours ago\n  \u002F\u002F Delete ALL data for this shop\n  await db.session.deleteMany({ where: { shop } });\n  await db.customer.deleteMany({ where: { shop } });\n  await db.order.deleteMany({ where: { shop } });\n  await db.settings.deleteMany({ where: { shop } });\n}\n```\n\n## Even if you store nothing\n\n```typescript\n\u002F\u002F You must still respond 200\ncase \"CUSTOMERS_DATA_REQUEST\":\ncase \"CUSTOMERS_REDACT\":\ncase \"SHOP_REDACT\":\n  \u002F\u002F No data stored, but must acknowledge\n  console.log(`GDPR ${topic} for ${shop} - no data stored`);\n  break;\n```\n\n## Validation Checks\n\n### Hardcoded Shopify API Secret\n\nSeverity: ERROR\n\nAPI secrets must never be hardcoded\n\nMessage: Hardcoded Shopify API secret. Use environment variables.\n\n### Hardcoded Shopify API Key\n\nSeverity: ERROR\n\nAPI keys should use environment variables\n\nMessage: Hardcoded Shopify API key. Use environment variables.\n\n### Missing HMAC Verification\n\nSeverity: ERROR\n\nWebhook endpoints must verify HMAC signature\n\nMessage: Webhook handler without HMAC verification. Use authenticate.webhook().\n\n### Synchronous Webhook Processing\n\nSeverity: WARNING\n\nWebhook handlers should respond quickly\n\nMessage: Multiple await calls in webhook handler. Consider async processing.\n\n### Missing Webhook Response\n\nSeverity: ERROR\n\nWebhooks must return 200 status\n\nMessage: Webhook handler may not return proper response.\n\n### Duplicate Webhook Registration\n\nSeverity: WARNING\n\nWebhooks should be defined in TOML only\n\nMessage: Code-based webhook registration. Define webhooks in shopify.app.toml.\n\n### REST API Usage\n\nSeverity: INFO\n\nREST API is deprecated, use GraphQL\n\nMessage: REST API usage detected. Consider migrating to GraphQL.\n\n### Missing Rate Limit Handling\n\nSeverity: WARNING\n\nAPI calls should handle 429 responses\n\nMessage: API call without rate limit handling. Implement retry logic.\n\n### In-Memory Session Storage\n\nSeverity: WARNING\n\nIn-memory sessions don't scale\n\nMessage: In-memory session storage. Use PrismaSessionStorage or similar.\n\n### Missing Session Validation\n\nSeverity: ERROR\n\nRoutes should validate session\n\nMessage: Loader without authentication. Use authenticate.admin(request).\n\n## Collaboration\n\n### Delegation Triggers\n\n- user needs payment processing -> stripe-integration (Shopify Payments or Stripe integration)\n- user needs custom authentication -> auth-specialist (Beyond Shopify OAuth)\n- user needs email\u002FSMS notifications -> twilio-communications (Customer notifications outside Shopify)\n- user needs AI features -> llm-architect (Product descriptions, chatbots)\n- user needs serverless deployment -> aws-serverless (Lambda or Vercel deployment)\n\n## When to Use\n- User mentions or implies: shopify app\n- User mentions or implies: shopify\n- User mentions or implies: embedded app\n- User mentions or implies: polaris\n- User mentions or implies: app bridge\n- User mentions or implies: shopify webhook\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,163,1328,"2026-05-16 13:40:20",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"编程开发","coding","mdi-code-braces","代码生成、调试、审查，提升开发效率",2,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"前端开发","frontend","mdi-language-html5","HTML\u002FCSS\u002FJavaScript\u002F框架相关",1,96,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"6c1d6e85-ce6b-4588-9cb0-dd757129a106","1.0.0","shopify-apps.zip",10837,"uploads\u002Fskills\u002Ff0af735f-f42f-43ee-afd8-f3089278c17e\u002Fshopify-apps.zip","e9f8c09bade55d203f47d6544974195b883a39b57ac1fa5d0cc2e4feda0b2a49","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":35275}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]