ChatGPT Claude Codex CLI Cohere Cursor DeepSeek Gemini GitHub Copilot GLM Grok Kimi Llama MiniMax Mistral OpenAI opencode Skill

shopify-apps

Expert patterns for Shopify app development including Remix/React

LLM Mart · 0 points · 21 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download sickn33-agentic-awesome-skills-skills_shopify-apps-286166a.zip · 10 KB
Part of sickn33/agentic-awesome-skills — 427 skills
This skill couldn't be refreshed from GitHub on the last check — you're seeing the last imported snapshot.

Install

skills CLI npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/shopify-apps
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
Git git clone https://github.com/sickn33/agentic-awesome-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Shopify Apps

Expert patterns for Shopify app development including Remix/React Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin API, Polaris components, billing, and app extensions.

Patterns

React Router App Setup

Modern Shopify app template with React Router

When to use: Starting a new Shopify app

Template

Create new Shopify app with CLI

npm init @shopify/app@latest my-shopify-app

Project structure

my-shopify-app/

├── app/

│ ├── routes/

│ │ ├── app._index.tsx # Main app page

│ │ ├── app.tsx # App layout with providers

│ │ ├── auth.$.tsx # Auth callback

│ │ └── webhooks.tsx # Webhook handler

│ ├── shopify.server.ts # Server configuration

│ └── root.tsx # Root layout

├── extensions/ # App extensions

├── shopify.app.toml # App configuration

└── package.json

// shopify.app.toml name = "my-shopify-app" client_id = "your-client-id" application_url = "https://your-app.example.com"

[access_scopes] scopes = "read_products,write_products,read_orders"

[webhooks] api_version = "2024-10"

[webhooks.subscriptions] topics = ["orders/create", "products/update"] uri = "/webhooks"

[auth] redirect_urls = ["https://your-app.example.com/auth/callback"]

// app/shopify.server.ts import "@shopify/shopify-app-remix/adapters/node"; import { LATEST_API_VERSION, shopifyApp, DeliveryMethod, } from "@shopify/shopify-app-remix/server"; import from "@shopify/shopify-app-session-storage-prisma"; import prisma from "./db.server";

const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY!, apiSecretKey: process.env.SHOPIFY_API_SECRET!, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL!, authPathPrefix: "/auth", sessionStorage: new PrismaSessionStorage(prisma), distribution: AppDistribution.AppStore, future: { unstable_newEmbeddedAuthStrategy: true, }, ...(process.env.SHOP_CUSTOM_DOMAIN ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] } : ), });

export default shopify; export const apiVersion = LATEST_API_VERSION; export const authenticate = shopify.authenticate; export const sessionStorage = shopify.sessionStorage;

Notes

  • React Router replaced Remix as recommended template (late 2024)
  • unstable_newEmbeddedAuthStrategy enabled by default for new apps
  • Webhooks configured in shopify.app.toml, not code
  • Run 'shopify app deploy' to apply configuration changes

Embedded App with App Bridge

Render app embedded in Shopify Admin

When to use: Building embedded admin app

Template

// app/routes/app.tsx - App layout with providers import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react"; import from "@shopify/shopify-app-remix/react"; import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export async function loader(: LoaderFunctionArgs) { await authenticate.admin(request); return json({ apiKey: process.env.SHOPIFY_API_KEY! }); }

export default function App() { const = useLoaderData

return ( ); }

export function ErrorBoundary() { const error = useRouteError(); return ( ); }

// app/routes/app._index.tsx - Main app page import { Page, Layout, Card, Text, BlockStack, Button, } from "@shopify/polaris"; import from "@shopify/app-bridge-react";

export async function loader(: LoaderFunctionArgs) { const = await authenticate.admin(request);

// GraphQL query const response = await admin.graphql(query { shop { name email } });

const = await response.json(); return json(); }

export default function Index() { const = useLoaderData

return ( ); }

Notes

  • App Bridge required for Built for Shopify (July 2025)
  • Polaris components match Shopify Admin design
  • TitleBar and navigation from App Bridge
  • Always authenticate requests with authenticate.admin()

Webhook Handling

Secure webhook processing with HMAC verification

When to use: Receiving Shopify webhooks

Template

// app/routes/webhooks.tsx import type from "@remix-run/node"; import from "../shopify.server"; import db from "../db.server";

export const action = async (: ActionFunctionArgs) => { // Authenticate webhook (verifies HMAC signature) const { topic, shop, payload, admin } = await authenticate.webhook(request);

console.log(Received ${topic} webhook for ${shop});

// Process based on topic switch (topic) { case "ORDERS_CREATE": // Queue for async processing await queueOrderProcessing(payload); break;

case "PRODUCTS_UPDATE":
  await handleProductUpdate(shop, payload);
  break;

case "APP_UNINSTALLED":
  // Clean up shop data
  await db.session.deleteMany({ where: { shop } });
  await db.shopData.delete({ where: { shop } });
  break;

case "CUSTOMERS_DATA_REQUEST":
case "CUSTOMERS_REDACT":
case "SHOP_REDACT":
  // GDPR webhooks - mandatory
  await handleGDPRWebhook(topic, payload);
  break;

default:
  console.log(`Unhandled webhook topic: ${topic}`);

}

// CRITICAL: Return 200 immediately // Shopify expects response within 5 seconds return new Response(null, { status: 200 }); };

// Process asynchronously after responding async function queueOrderProcessing(payload: any) { // Use a job queue (BullMQ, etc.) await jobQueue.add("process-order", { orderId: payload.id, orderData: payload, }); }

async function handleProductUpdate(shop: string, payload: any) { // Quick sync operation only await db.product.upsert({ where: , update: { title: payload.title, updatedAt: new Date(), }, create: { shopifyId: payload.id, shop, title: payload.title, }, }); }

async function handleGDPRWebhook(topic: string, payload: any) { // GDPR compliance - required for all apps switch (topic) { case "CUSTOMERS_DATA_REQUEST": // Return customer data within 30 days break; case "CUSTOMERS_REDACT": // Delete customer data break; case "SHOP_REDACT": // Delete all shop data (48 hours after uninstall) break; } }

Notes

  • Respond within 5 seconds or webhook fails
  • Use job queues for heavy processing
  • GDPR webhooks are mandatory for App Store
  • HMAC verification handled by authenticate.webhook()

GraphQL Admin API

Query and mutate shop data with GraphQL

When to use: Interacting with Shopify Admin API

Template

// GraphQL queries with authenticated admin client export async function loader(: LoaderFunctionArgs) { const = await authenticate.admin(request);

// Query products with pagination const response = await admin.graphql(query GetProducts($first: Int!, $after: String) { products(first: $first, after: $after) { edges { node { id title status totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } } images(first: 1) { edges { node { url altText } } } } cursor } pageInfo { hasNextPage endCursor } } }, { variables: { first: 10, after: null, }, });

const = await response.json(); return json(); }

// Mutations export async function action(: ActionFunctionArgs) { const = await authenticate.admin(request); const formData = await request.formData(); const productId = formData.get("productId"); const newTitle = formData.get("title");

const response = await admin.graphql(mutation UpdateProduct($input: ProductInput!) { productUpdate(input: $input) { product { id title } userErrors { field message } } }, { variables: { input: { id: productId, title: newTitle, }, }, });

const = await response.json();

if (data.productUpdate.userErrors.length > 0) { return json({ errors: data.productUpdate.userErrors, }, { status: 400 }); }

return json(); }

// Bulk operations for large datasets async function bulkUpdateProducts(admin: AdminApiContext) { // Create bulk operation const response = await admin.graphql(mutation { bulkOperationRunMutation( mutation: "mutation call($input: ProductInput!) { productUpdate(input: $input) { product { id } } }", stagedUploadPath: "path-to-staged-upload" ) { bulkOperation { id status } userErrors { message } } });

// Poll for completion or use webhook // BULK_OPERATIONS_FINISH webhook }

Notes

  • GraphQL required for new public apps (April 2025)
  • Rate limit: 1000 points per 60 seconds
  • Use bulk operations for >250 items
  • Direct API access available from App Bridge

Billing API Integration

Implement subscription billing for your app

When to use: Monetizing Shopify app

Template

// app/routes/app.billing.tsx import { json, redirect } from "@remix-run/node"; import { Page, Card, Button, BlockStack, Text } from "@shopify/polaris"; import from "../shopify.server";

const PLANS = { basic: { name: "Basic", amount: 9.99, currencyCode: "USD", interval: "EVERY_30_DAYS", }, pro: { name: "Pro", amount: 29.99, currencyCode: "USD", interval: "EVERY_30_DAYS", }, };

export async function loader(: LoaderFunctionArgs) { const { admin, billing } = await authenticate.admin(request);

// Check current subscription const response = await admin.graphql(query { currentAppInstallation { activeSubscriptions { id name status lineItems { plan { pricingDetails { ... on AppRecurringPricing { price { amount currencyCode } interval } } } } } } });

const = await response.json(); return json({ subscription: data.currentAppInstallation.activeSubscriptions[0], }); }

export async function action(: ActionFunctionArgs) { const { admin, session } = await authenticate.admin(request); const formData = await request.formData(); const planKey = formData.get("plan") as keyof typeof PLANS; const plan = PLANS[planKey];

// Create subscription charge const response = await admin.graphql(mutation CreateSubscription($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $test: Boolean) { appSubscriptionCreate( name: $name lineItems: $lineItems returnUrl: $returnUrl test: $test ) { appSubscription { id status } confirmationUrl userErrors { field message } } }, { variables: { name: plan.name, lineItems: [ { plan: { appRecurringPricingDetails: { price: { amount: plan.amount, currencyCode: plan.currencyCode, }, interval: plan.interval, }, }, }, ], returnUrl: https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}, test: process.env.NODE_ENV !== "production", }, });

const = await response.json();

if (data.appSubscriptionCreate.userErrors.length > 0) { return json({ errors: data.appSubscriptionCreate.userErrors, }, { status: 400 }); }

// Redirect merchant to approve charge return redirect(data.appSubscriptionCreate.confirmationUrl); }

export default function Billing() { const = useLoaderData

return ( ); }

Notes

  • Use test: true for development stores
  • Merchant must approve subscription
  • One recurring + one usage charge per app max
  • 30-day billing cycle for recurring charges

App Extension Development

Extend Shopify checkout, admin, or storefront

When to use: Building app extensions

Template

shopify.extension.toml (in extensions/my-extension/)

api_version = "2024-10"

[[extensions]] type = "ui_extension" name = "Product Customizer" handle = "product-customizer"

[[extensions.targeting]] target = "admin.product-details.block.render" module = "./src/AdminBlock.tsx"

[extensions.capabilities] api_access = true

[extensions.settings] [[extensions.settings.fields]] key = "show_preview" type = "boolean" name = "Show Preview"

// extensions/my-extension/src/AdminBlock.tsx import { reactExtension, useApi, useSettings, BlockStack, Text, Button, InlineStack, } from "@shopify/ui-extensions-react/admin";

export default reactExtension( "admin.product-details.block.render", () =>

function ProductCustomizer() { const { data, extension } = useApi<"admin.product-details.block.render">(); const settings = useSettings();

const productId = data?.selected?.[0]?.id;

const handleCustomize = async () => { // API calls from extension const result = await fetch("/api/customize", { method: "POST", body: JSON.stringify(), }); };

return ( ); }

// Checkout UI Extension // [[extensions.targeting]] // target = "purchase.checkout.block.render"

// extensions/checkout-ext/src/Checkout.tsx import { reactExtension, Banner, useCartLines, useTotalAmount, } from "@shopify/ui-extensions-react/checkout";

export default reactExtension( "purchase.checkout.block.render", () =>

function CheckoutBanner() { const cartLines = useCartLines(); const total = useTotalAmount();

if (total.amount > 100) { return ( ); }

return null; }

Notes

  • Extensions run in sandboxed iframe
  • Use @shopify/ui-extensions-react for React
  • Limited APIs compared to full app
  • Deploy with 'shopify app deploy'

Sharp Edges

Webhook Must Respond Within 5 Seconds

Severity: HIGH

Situation: Receiving webhooks from Shopify

Symptoms: Webhook deliveries marked as failed. "Your app didn't respond in time" in Shopify logs. Missing order/product updates. Webhooks retried repeatedly then cancelled.

Why this breaks: Shopify expects a 2xx response within 5 seconds. If your app processes the webhook data before responding, you'll timeout.

Shopify retries failed webhooks up to 19 times over 48 hours. After continued failures, webhooks may be cancelled entirely.

Heavy processing (API calls, database operations) must happen after the response is sent.

Recommended fix:

Respond immediately, process asynchronously

// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, shop, payload } = await authenticate.webhook(request);

  // Queue for async processing
  await jobQueue.add("process-webhook", {
    topic,
    shop,
    payload,
  });

  // CRITICAL: Return 200 immediately
  return new Response(null, { status: 200 });
};

// Worker process handles the actual work
// workers/webhook-processor.ts
import { Worker } from "bullmq";

const worker = new Worker("process-webhook", async (job) => {
  const { topic, shop, payload } = job.data;

  switch (topic) {
    case "ORDERS_CREATE":
      await processOrder(shop, payload);
      break;
    // ... other handlers
  }
});

For simple operations, be quick

// Simple database update is OK if fast
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, payload } = await authenticate.webhook(request);

  // Quick database update (< 1 second)
  await db.product.update({
    where: { shopifyId: payload.id },
    data: { title: payload.title },
  });

  return new Response(null, { status: 200 });
};

Monitor webhook performance

// Log response times
const start = Date.now();

await handleWebhook(payload);

const duration = Date.now() - start;
console.log(`Webhook processed in ${duration}ms`);

// Alert if approaching timeout
if (duration > 3000) {
  console.warn("Webhook processing taking too long!");
}

API Rate Limits Cause 429 Errors

Severity: HIGH

Situation: Making API calls to Shopify

Symptoms: HTTP 429 Too Many Requests errors. "Throttled" responses. App becomes unresponsive. Operations fail silently or partially.

Why this breaks: Shopify enforces strict rate limits:

  • REST: 2 requests per second per store
  • GraphQL: 1000 points per 60 seconds

Exceeding limits causes immediate 429 errors. Continuous violations can result in temporary bans.

Bulk operations count against limits.

Recommended fix:

Check rate limit headers

// REST API
// X-Shopify-Shop-Api-Call-Limit: 39/40

// GraphQL - check response extensions
const response = await admin.graphql(`...`);
const { data, extensions } = await response.json();

const cost = extensions?.cost;
// {
//   "requestedQueryCost": 42,
//   "actualQueryCost": 42,
//   "throttleStatus": {
//     "maximumAvailable": 1000,
//     "currentlyAvailable": 958,
//     "restoreRate": 50
//   }
// }

Implement retry with exponential backoff

async function shopifyRequest(
  fn: () => Promise<Response>,
  maxRetries = 3
): Promise<Response> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fn();

      if (response.status === 429) {
        // Get retry-after header or default
        const retryAfter = parseInt(
          response.headers.get("Retry-After") || "2"
        );
        await sleep(retryAfter * 1000 * Math.pow(2, attempt));
        continue;
      }

      return response;
    } catch (error) {
      lastError = error as Error;
    }
  }

  throw lastError!;
}

Use bulk operations for large datasets

// Instead of 1000 individual calls, use bulk mutation
const response = await admin.graphql(`
  mutation {
    bulkOperationRunMutation(
      mutation: "mutation($input: ProductInput!) {
        productUpdate(input: $input) { product { id } }
      }",
      stagedUploadPath: "..."
    ) {
      bulkOperation { id status }
      userErrors { message }
    }
  }
`);

Queue requests

import { RateLimiter } from "limiter";

// 2 requests per second for REST
const limiter = new RateLimiter({
  tokensPerInterval: 2,
  interval: "second",
});

async function rateLimitedRequest(fn: () => Promise<any>) {
  await limiter.removeTokens(1);
  return fn();
}

Protected Customer Data Requires Special Permission

Severity: HIGH

Situation: Accessing customer PII in webhooks or API

Symptoms: Webhook deliveries fail for orders/customers. Customer data fields are null or empty. App works in development but fails in production. "Protected customer data access" errors.

Why this breaks: Since April 2024, accessing protected customer data (PII) requires explicit approval from Shopify. This is separate from OAuth scopes.

Protected data includes:

  • Customer names, emails, addresses
  • Order customer information
  • Subscription customer details

Even with read_orders scope, you won't receive customer data in webhooks without protected data access.

Recommended fix:

Request protected customer data access

  1. Go to Partner Dashboard > App > API access
  2. Under "Protected customer data access"
  3. Request access for needed data types
  4. Justify your use case
  5. Wait for Shopify approval (can take days)

Check your data access level

// Query your app's data access
const response = await admin.graphql(`
  query {
    currentAppInstallation {
      accessScopes {
        handle
      }
    }
  }
`);

Handle missing data gracefully

// Webhook payload may have redacted fields
async function processOrder(payload: any) {
  const customerEmail = payload.customer?.email;

  if (!customerEmail) {
    // Customer data not available
    // Either no protected access or data redacted
    console.log("Customer data not available");
    return;
  }

  await sendOrderConfirmation(customerEmail);
}

Use customer account API for direct access

// If customer is logged in, can access their data
// through Customer Account API (different from Admin API)

Duplicate Webhook Definitions Cause Conflicts

Severity: MEDIUM

Situation: Configuring webhooks in both TOML and code

Symptoms: Duplicate webhook deliveries. Some webhooks fire twice. Webhook subscriptions fail to register. Unpredictable webhook behavior.

Why this breaks: Shopify apps can define webhooks in two places:

  1. shopify.app.toml (declarative, recommended)
  2. afterAuth hook in code (imperative, legacy)

If you define the same webhook in both places, you get:

  • Duplicate subscriptions
  • Race conditions during registration
  • Conflicts during app updates

Recommended fix:

Use TOML only (recommended)

# shopify.app.toml
[webhooks]
api_version = "2024-10"

[webhooks.subscriptions]
topics = [
  "orders/create",
  "orders/updated",
  "products/create",
  "products/update",
  "app/uninstalled"
]
uri = "/webhooks"

Remove code-based registration

// DON'T do this if using TOML
const shopify = shopifyApp({
  // ...
  hooks: {
    afterAuth: async ({ session }) => {
      // Remove webhook registration from here
      // Let TOML handle it
    },
  },
});

Deploy to apply TOML changes

# Webhooks registered on deploy
shopify app deploy

Check current subscriptions

const response = await admin.graphql(`
  query {
    webhookSubscriptions(first: 50) {
      edges {
        node {
          id
          topic
          endpoint {
            ... on WebhookHttpEndpoint {
              callbackUrl
            }
          }
        }
      }
    }
  }
`);

Webhook URL Trailing Slash Causes 404

Severity: MEDIUM

Situation: Setting up webhook endpoints

Symptoms: Webhooks return 404 Not Found. Webhook delivery fails immediately. Works in local dev but fails in production. Logs show request to /webhooks/ not /webhooks.

Why this breaks: Shopify automatically adds a trailing slash to webhook URLs. If your server doesn't handle both /webhooks and /webhooks/, the webhook will 404.

Common with frameworks that are strict about trailing slashes.

Recommended fix:

Handle both URL formats

// Remix/React Router - both work by default
// app/routes/webhooks.tsx handles /webhooks

// Express - add middleware
app.use((req, res, next) => {
  if (req.path.endsWith('/') && req.path.length > 1) {
    const query = req.url.slice(req.path.length);
    const safePath = req.path.slice(0, -1);
    res.redirect(301, safePath + query);
  }
  next();
});

Configure web server

# Nginx - strip trailing slashes
location ~ ^(.+)/$ {
  return 301 $1;
}

# Or rewrite to handler
location /webhooks {
  try_files $uri $uri/ @webhooks;
}
location @webhooks {
  proxy_pass http://app:3000/webhooks;
}

Test both formats

# Test without slash
curl -X POST https://your-app.com/webhooks

# Test with slash
curl -X POST https://your-app.com/webhooks/

REST API Required Migration to GraphQL (April 2025)

Severity: HIGH

Situation: Building new public apps or maintaining existing

Symptoms: App store submission rejected for REST API usage. Deprecation warnings in console. Some REST endpoints stop working. Missing features only in GraphQL.

Why this breaks: As of October 2024, REST Admin API is legacy. Starting April 2025, new public apps MUST use GraphQL.

REST endpoints will continue working for existing apps, but new features are GraphQL-only.

Metafields, bulk operations, and many new features require GraphQL.

Recommended fix:

Use GraphQL for all new code

// REST (legacy)
const response = await fetch(
  `https://${shop}/admin/api/2024-10/products.json`,
  {
    headers: { "X-Shopify-Access-Token": token },
  }
);

// GraphQL (recommended)
const response = await admin.graphql(`
  query {
    products(first: 10) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
`);

Migrate existing REST calls

// REST: GET /products/{id}.json
// GraphQL equivalent:
const response = await admin.graphql(`
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      title
      status
      variants(first: 10) {
        edges {
          node {
            id
            price
            inventoryQuantity
          }
        }
      }
    }
  }
`, {
  variables: { id: `gid://shopify/Product/${productId}` },
});

Use GraphQL for webhooks too

# shopify.app.toml
[webhooks]
api_version = "2024-10"  # Use latest GraphQL version

App Bridge Required for Built for Shopify (July 2025)

Severity: HIGH

Situation: Building embedded Shopify apps

Symptoms: App rejected from "Built for Shopify" program. App not appearing correctly in admin. Navigation and chrome issues. Warning about App Bridge version.

Why this breaks: Effective July 2025, all apps seeking "Built for Shopify" status must use the latest version of App Bridge and be embedded.

Apps using old App Bridge versions or not embedded will lose built for Shopify benefits (better placement, badges).

Shopify now serves App Bridge and Polaris via unversioned script tags that auto-update.

Recommended fix:

Use latest App Bridge via script tag

<!-- Automatically stays up to date -->
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>

Use AppProvider in React

// app/routes/app.tsx
import { AppProvider } from "@shopify/shopify-app-remix/react";

export default function App() {
  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <Outlet />
    </AppProvider>
  );
}

Enable embedded auth strategy

// shopify.server.ts
const shopify = shopifyApp({
  // ...
  future: {
    unstable_newEmbeddedAuthStrategy: true,
  },
});

Check embedded status

import { useAppBridge } from "@shopify/app-bridge-react";

function MyComponent() {
  const app = useAppBridge();
  const isEmbedded = app.hostOrigin !== window.location.origin;
}

Missing GDPR Webhooks Block App Store Approval

Severity: HIGH

Situation: Submitting app to Shopify App Store

Symptoms: App submission rejected. "GDPR webhooks not implemented" error. Manual review fails for compliance. Data request webhooks not handled.

Why this breaks: Shopify requires all apps to handle three GDPR webhooks:

  1. customers/data_request - Provide customer data
  2. customers/redact - Delete customer data
  3. shop/redact - Delete all shop data

These are automatically subscribed when you create an app. You MUST implement handlers even if you don't store data.

Recommended fix:

Implement all GDPR handlers

// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, payload, shop } = await authenticate.webhook(request);

  switch (topic) {
    case "CUSTOMERS_DATA_REQUEST":
      await handleDataRequest(shop, payload);
      break;

    case "CUSTOMERS_REDACT":
      await handleCustomerRedact(shop, payload);
      break;

    case "SHOP_REDACT":
      await handleShopRedact(shop, payload);
      break;
  }

  return new Response(null, { status: 200 });
};

async function handleDataRequest(shop: string, payload: any) {
  const customerId = payload.customer.id;

  // Return customer data within 30 days
  // Usually send to data_request.destination_url
  const customerData = await db.customer.findUnique({
    where: { shopifyId: customerId, shop },
  });

  if (customerData) {
    // Send to provided URL or email
    await sendDataToMerchant(payload.data_request, customerData);
  }
}

async function handleCustomerRedact(shop: string, payload: any) {
  const customerId = payload.customer.id;

  // Delete customer's personal data
  await db.customer.deleteMany({
    where: { shopifyId: customerId, shop },
  });

  await db.order.updateMany({
    where: { customerId, shop },
    data: { customerEmail: null, customerName: null },
  });
}

async function handleShopRedact(shop: string, payload: any) {
  // Shop uninstalled 48+ hours ago
  // Delete ALL data for this shop
  await db.session.deleteMany({ where: { shop } });
  await db.customer.deleteMany({ where: { shop } });
  await db.order.deleteMany({ where: { shop } });
  await db.settings.deleteMany({ where: { shop } });
}

Even if you store nothing

// You must still respond 200
case "CUSTOMERS_DATA_REQUEST":
case "CUSTOMERS_REDACT":
case "SHOP_REDACT":
  // No data stored, but must acknowledge
  console.log(`GDPR ${topic} for ${shop} - no data stored`);
  break;

Validation Checks

Hardcoded Shopify API Secret

Severity: ERROR

API secrets must never be hardcoded

Message: Hardcoded Shopify API secret. Use environment variables.

Hardcoded Shopify API Key

Severity: ERROR

API keys should use environment variables

Message: Hardcoded Shopify API key. Use environment variables.

Missing HMAC Verification

Severity: ERROR

Webhook endpoints must verify HMAC signature

Message: Webhook handler without HMAC verification. Use authenticate.webhook().

Synchronous Webhook Processing

Severity: WARNING

Webhook handlers should respond quickly

Message: Multiple await calls in webhook handler. Consider async processing.

Missing Webhook Response

Severity: ERROR

Webhooks must return 200 status

Message: Webhook handler may not return proper response.

Duplicate Webhook Registration

Severity: WARNING

Webhooks should be defined in TOML only

Message: Code-based webhook registration. Define webhooks in shopify.app.toml.

REST API Usage

Severity: INFO

REST API is deprecated, use GraphQL

Message: REST API usage detected. Consider migrating to GraphQL.

Missing Rate Limit Handling

Severity: WARNING

API calls should handle 429 responses

Message: API call without rate limit handling. Implement retry logic.

In-Memory Session Storage

Severity: WARNING

In-memory sessions don't scale

Message: In-memory session storage. Use PrismaSessionStorage or similar.

Missing Session Validation

Severity: ERROR

Routes should validate session

Message: Loader without authentication. Use authenticate.admin(request).

Collaboration

Delegation Triggers

  • user needs payment processing -> stripe-integration (Shopify Payments or Stripe integration)
  • user needs custom authentication -> auth-specialist (Beyond Shopify OAuth)
  • user needs email/SMS notifications -> twilio-communications (Customer notifications outside Shopify)
  • user needs AI features -> llm-architect (Product descriptions, chatbots)
  • user needs serverless deployment -> aws-serverless (Lambda or Vercel deployment)

When to Use

  • User mentions or implies: shopify app
  • User mentions or implies: shopify
  • User mentions or implies: embedded app
  • User mentions or implies: polaris
  • User mentions or implies: app bridge
  • User mentions or implies: shopify webhook

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
Files (agentic-awesome-skills)
  • SKILL.md 34.4 KB
    ---
    name: shopify-apps
    description: Expert patterns for Shopify app development including Remix/React
      Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin
      API, Polaris components, billing, and app extensions.
    risk: safe
    source: vibeship-spawner-skills (Apache 2.0)
    date_added: 2026-02-27
    ---
    
    # Shopify Apps
    
    Expert patterns for Shopify app development including Remix/React Router apps,
    embedded apps with App Bridge, webhook handling, GraphQL Admin API,
    Polaris components, billing, and app extensions.
    
    ## Patterns
    
    ### React Router App Setup
    
    Modern Shopify app template with React Router
    
    **When to use**: Starting a new Shopify app
    
    ### Template
    
    # Create new Shopify app with CLI
    npm init @shopify/app@latest my-shopify-app
    
    # Project structure
    # my-shopify-app/
    # ├── app/
    # │   ├── routes/
    # │   │   ├── app._index.tsx        # Main app page
    # │   │   ├── app.tsx               # App layout with providers
    # │   │   ├── auth.$.tsx            # Auth callback
    # │   │   └── webhooks.tsx          # Webhook handler
    # │   ├── shopify.server.ts         # Server configuration
    # │   └── root.tsx                  # Root layout
    # ├── extensions/                   # App extensions
    # ├── shopify.app.toml              # App configuration
    # └── package.json
    
    // shopify.app.toml
    name = "my-shopify-app"
    client_id = "your-client-id"
    application_url = "https://your-app.example.com"
    
    [access_scopes]
    scopes = "read_products,write_products,read_orders"
    
    [webhooks]
    api_version = "2024-10"
    
    [webhooks.subscriptions]
    topics = ["orders/create", "products/update"]
    uri = "/webhooks"
    
    [auth]
    redirect_urls = ["https://your-app.example.com/auth/callback"]
    
    // app/shopify.server.ts
    import "@shopify/shopify-app-remix/adapters/node";
    import {
      LATEST_API_VERSION,
      shopifyApp,
      DeliveryMethod,
    } from "@shopify/shopify-app-remix/server";
    import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
    import prisma from "./db.server";
    
    const shopify = shopifyApp({
      apiKey: process.env.SHOPIFY_API_KEY!,
      apiSecretKey: process.env.SHOPIFY_API_SECRET!,
      scopes: process.env.SCOPES?.split(","),
      appUrl: process.env.SHOPIFY_APP_URL!,
      authPathPrefix: "/auth",
      sessionStorage: new PrismaSessionStorage(prisma),
      distribution: AppDistribution.AppStore,
      future: {
        unstable_newEmbeddedAuthStrategy: true,
      },
      ...(process.env.SHOP_CUSTOM_DOMAIN
        ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
        : {}),
    });
    
    export default shopify;
    export const apiVersion = LATEST_API_VERSION;
    export const authenticate = shopify.authenticate;
    export const sessionStorage = shopify.sessionStorage;
    
    ### Notes
    
    - React Router replaced Remix as recommended template (late 2024)
    - unstable_newEmbeddedAuthStrategy enabled by default for new apps
    - Webhooks configured in shopify.app.toml, not code
    - Run 'shopify app deploy' to apply configuration changes
    
    ### Embedded App with App Bridge
    
    Render app embedded in Shopify Admin
    
    **When to use**: Building embedded admin app
    
    ### Template
    
    // app/routes/app.tsx - App layout with providers
    import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
    import { AppProvider } from "@shopify/shopify-app-remix/react";
    import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
    
    export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
    
    export async function loader({ request }: LoaderFunctionArgs) {
      await authenticate.admin(request);
      return json({ apiKey: process.env.SHOPIFY_API_KEY! });
    }
    
    export default function App() {
      const { apiKey } = useLoaderData<typeof loader>();
    
      return (
        <AppProvider isEmbeddedApp apiKey={apiKey}>
          <ui-nav-menu>
            <Link to="/app" rel="home">Home</Link>
            <Link to="/app/products">Products</Link>
            <Link to="/app/settings">Settings</Link>
          </ui-nav-menu>
          <Outlet />
        </AppProvider>
      );
    }
    
    export function ErrorBoundary() {
      const error = useRouteError();
      return (
        <AppProvider isEmbeddedApp>
          <Page>
            <Card>
              <Text as="p" variant="bodyMd">
                Something went wrong. Please try again.
              </Text>
            </Card>
          </Page>
        </AppProvider>
      );
    }
    
    // app/routes/app._index.tsx - Main app page
    import {
      Page,
      Layout,
      Card,
      Text,
      BlockStack,
      Button,
    } from "@shopify/polaris";
    import { TitleBar } from "@shopify/app-bridge-react";
    
    export async function loader({ request }: LoaderFunctionArgs) {
      const { admin } = await authenticate.admin(request);
    
      // GraphQL query
      const response = await admin.graphql(`
        query {
          shop {
            name
            email
          }
        }
      `);
    
      const { data } = await response.json();
      return json({ shop: data.shop });
    }
    
    export default function Index() {
      const { shop } = useLoaderData<typeof loader>();
    
      return (
        <Page>
          <TitleBar title="My Shopify App" />
          <Layout>
            <Layout.Section>
              <Card>
                <BlockStack gap="200">
                  <Text as="h2" variant="headingMd">
                    Welcome to {shop.name}!
                  </Text>
                  <Text as="p" variant="bodyMd">
                    Your app is now connected to this store.
                  </Text>
                  <Button variant="primary">
                    Get Started
                  </Button>
                </BlockStack>
              </Card>
            </Layout.Section>
          </Layout>
        </Page>
      );
    }
    
    ### Notes
    
    - App Bridge required for Built for Shopify (July 2025)
    - Polaris components match Shopify Admin design
    - TitleBar and navigation from App Bridge
    - Always authenticate requests with authenticate.admin()
    
    ### Webhook Handling
    
    Secure webhook processing with HMAC verification
    
    **When to use**: Receiving Shopify webhooks
    
    ### Template
    
    // app/routes/webhooks.tsx
    import type { ActionFunctionArgs } from "@remix-run/node";
    import { authenticate } from "../shopify.server";
    import db from "../db.server";
    
    export const action = async ({ request }: ActionFunctionArgs) => {
      // Authenticate webhook (verifies HMAC signature)
      const { topic, shop, payload, admin } = await authenticate.webhook(request);
    
      console.log(`Received ${topic} webhook for ${shop}`);
    
      // Process based on topic
      switch (topic) {
        case "ORDERS_CREATE":
          // Queue for async processing
          await queueOrderProcessing(payload);
          break;
    
        case "PRODUCTS_UPDATE":
          await handleProductUpdate(shop, payload);
          break;
    
        case "APP_UNINSTALLED":
          // Clean up shop data
          await db.session.deleteMany({ where: { shop } });
          await db.shopData.delete({ where: { shop } });
          break;
    
        case "CUSTOMERS_DATA_REQUEST":
        case "CUSTOMERS_REDACT":
        case "SHOP_REDACT":
          // GDPR webhooks - mandatory
          await handleGDPRWebhook(topic, payload);
          break;
    
        default:
          console.log(`Unhandled webhook topic: ${topic}`);
      }
    
      // CRITICAL: Return 200 immediately
      // Shopify expects response within 5 seconds
      return new Response(null, { status: 200 });
    };
    
    // Process asynchronously after responding
    async function queueOrderProcessing(payload: any) {
      // Use a job queue (BullMQ, etc.)
      await jobQueue.add("process-order", {
        orderId: payload.id,
        orderData: payload,
      });
    }
    
    async function handleProductUpdate(shop: string, payload: any) {
      // Quick sync operation only
      await db.product.upsert({
        where: { shopifyId: payload.id },
        update: {
          title: payload.title,
          updatedAt: new Date(),
        },
        create: {
          shopifyId: payload.id,
          shop,
          title: payload.title,
        },
      });
    }
    
    async function handleGDPRWebhook(topic: string, payload: any) {
      // GDPR compliance - required for all apps
      switch (topic) {
        case "CUSTOMERS_DATA_REQUEST":
          // Return customer data within 30 days
          break;
        case "CUSTOMERS_REDACT":
          // Delete customer data
          break;
        case "SHOP_REDACT":
          // Delete all shop data (48 hours after uninstall)
          break;
      }
    }
    
    ### Notes
    
    - Respond within 5 seconds or webhook fails
    - Use job queues for heavy processing
    - GDPR webhooks are mandatory for App Store
    - HMAC verification handled by authenticate.webhook()
    
    ### GraphQL Admin API
    
    Query and mutate shop data with GraphQL
    
    **When to use**: Interacting with Shopify Admin API
    
    ### Template
    
    // GraphQL queries with authenticated admin client
    export async function loader({ request }: LoaderFunctionArgs) {
      const { admin } = await authenticate.admin(request);
    
      // Query products with pagination
      const response = await admin.graphql(`
        query GetProducts($first: Int!, $after: String) {
          products(first: $first, after: $after) {
            edges {
              node {
                id
                title
                status
                totalInventory
                priceRangeV2 {
                  minVariantPrice {
                    amount
                    currencyCode
                  }
                }
                images(first: 1) {
                  edges {
                    node {
                      url
                      altText
                    }
                  }
                }
              }
              cursor
            }
            pageInfo {
              hasNextPage
              endCursor
            }
          }
        }
      `, {
        variables: {
          first: 10,
          after: null,
        },
      });
    
      const { data } = await response.json();
      return json({ products: data.products });
    }
    
    // Mutations
    export async function action({ request }: ActionFunctionArgs) {
      const { admin } = await authenticate.admin(request);
      const formData = await request.formData();
      const productId = formData.get("productId");
      const newTitle = formData.get("title");
    
      const response = await admin.graphql(`
        mutation UpdateProduct($input: ProductInput!) {
          productUpdate(input: $input) {
            product {
              id
              title
            }
            userErrors {
              field
              message
            }
          }
        }
      `, {
        variables: {
          input: {
            id: productId,
            title: newTitle,
          },
        },
      });
    
      const { data } = await response.json();
    
      if (data.productUpdate.userErrors.length > 0) {
        return json({
          errors: data.productUpdate.userErrors,
        }, { status: 400 });
      }
    
      return json({ product: data.productUpdate.product });
    }
    
    // Bulk operations for large datasets
    async function bulkUpdateProducts(admin: AdminApiContext) {
      // Create bulk operation
      const response = await admin.graphql(`
        mutation {
          bulkOperationRunMutation(
            mutation: "mutation call($input: ProductInput!) {
              productUpdate(input: $input) { product { id } }
            }",
            stagedUploadPath: "path-to-staged-upload"
          ) {
            bulkOperation {
              id
              status
            }
            userErrors {
              message
            }
          }
        }
      `);
    
      // Poll for completion or use webhook
      // BULK_OPERATIONS_FINISH webhook
    }
    
    ### Notes
    
    - GraphQL required for new public apps (April 2025)
    - Rate limit: 1000 points per 60 seconds
    - Use bulk operations for >250 items
    - Direct API access available from App Bridge
    
    ### Billing API Integration
    
    Implement subscription billing for your app
    
    **When to use**: Monetizing Shopify app
    
    ### Template
    
    // app/routes/app.billing.tsx
    import { json, redirect } from "@remix-run/node";
    import { Page, Card, Button, BlockStack, Text } from "@shopify/polaris";
    import { authenticate } from "../shopify.server";
    
    const PLANS = {
      basic: {
        name: "Basic",
        amount: 9.99,
        currencyCode: "USD",
        interval: "EVERY_30_DAYS",
      },
      pro: {
        name: "Pro",
        amount: 29.99,
        currencyCode: "USD",
        interval: "EVERY_30_DAYS",
      },
    };
    
    export async function loader({ request }: LoaderFunctionArgs) {
      const { admin, billing } = await authenticate.admin(request);
    
      // Check current subscription
      const response = await admin.graphql(`
        query {
          currentAppInstallation {
            activeSubscriptions {
              id
              name
              status
              lineItems {
                plan {
                  pricingDetails {
                    ... on AppRecurringPricing {
                      price {
                        amount
                        currencyCode
                      }
                      interval
                    }
                  }
                }
              }
            }
          }
        }
      `);
    
      const { data } = await response.json();
      return json({
        subscription: data.currentAppInstallation.activeSubscriptions[0],
      });
    }
    
    export async function action({ request }: ActionFunctionArgs) {
      const { admin, session } = await authenticate.admin(request);
      const formData = await request.formData();
      const planKey = formData.get("plan") as keyof typeof PLANS;
      const plan = PLANS[planKey];
    
      // Create subscription charge
      const response = await admin.graphql(`
        mutation CreateSubscription($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $test: Boolean) {
          appSubscriptionCreate(
            name: $name
            lineItems: $lineItems
            returnUrl: $returnUrl
            test: $test
          ) {
            appSubscription {
              id
              status
            }
            confirmationUrl
            userErrors {
              field
              message
            }
          }
        }
      `, {
        variables: {
          name: plan.name,
          lineItems: [
            {
              plan: {
                appRecurringPricingDetails: {
                  price: {
                    amount: plan.amount,
                    currencyCode: plan.currencyCode,
                  },
                  interval: plan.interval,
                },
              },
            },
          ],
          returnUrl: `https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}`,
          test: process.env.NODE_ENV !== "production",
        },
      });
    
      const { data } = await response.json();
    
      if (data.appSubscriptionCreate.userErrors.length > 0) {
        return json({
          errors: data.appSubscriptionCreate.userErrors,
        }, { status: 400 });
      }
    
      // Redirect merchant to approve charge
      return redirect(data.appSubscriptionCreate.confirmationUrl);
    }
    
    export default function Billing() {
      const { subscription } = useLoaderData<typeof loader>();
      const submit = useSubmit();
    
      return (
        <Page title="Billing">
          <Card>
            {subscription ? (
              <BlockStack gap="200">
                <Text as="p" variant="bodyMd">
                  Current plan: {subscription.name}
                </Text>
                <Text as="p" variant="bodyMd">
                  Status: {subscription.status}
                </Text>
              </BlockStack>
            ) : (
              <BlockStack gap="400">
                <Text as="h2" variant="headingMd">
                  Choose a Plan
                </Text>
                <Button onClick={() => submit({ plan: "basic" }, { method: "post" })}>
                  Basic - $9.99/month
                </Button>
                <Button onClick={() => submit({ plan: "pro" }, { method: "post" })}>
                  Pro - $29.99/month
                </Button>
              </BlockStack>
            )}
          </Card>
        </Page>
      );
    }
    
    ### Notes
    
    - Use test: true for development stores
    - Merchant must approve subscription
    - One recurring + one usage charge per app max
    - 30-day billing cycle for recurring charges
    
    ### App Extension Development
    
    Extend Shopify checkout, admin, or storefront
    
    **When to use**: Building app extensions
    
    ### Template
    
    # shopify.extension.toml (in extensions/my-extension/)
    api_version = "2024-10"
    
    [[extensions]]
    type = "ui_extension"
    name = "Product Customizer"
    handle = "product-customizer"
    
    [[extensions.targeting]]
    target = "admin.product-details.block.render"
    module = "./src/AdminBlock.tsx"
    
    [extensions.capabilities]
    api_access = true
    
    [extensions.settings]
    [[extensions.settings.fields]]
    key = "show_preview"
    type = "boolean"
    name = "Show Preview"
    
    // extensions/my-extension/src/AdminBlock.tsx
    import {
      reactExtension,
      useApi,
      useSettings,
      BlockStack,
      Text,
      Button,
      InlineStack,
    } from "@shopify/ui-extensions-react/admin";
    
    export default reactExtension(
      "admin.product-details.block.render",
      () => <ProductCustomizer />
    );
    
    function ProductCustomizer() {
      const { data, extension } = useApi<"admin.product-details.block.render">();
      const settings = useSettings();
    
      const productId = data?.selected?.[0]?.id;
    
      const handleCustomize = async () => {
        // API calls from extension
        const result = await fetch("/api/customize", {
          method: "POST",
          body: JSON.stringify({ productId }),
        });
      };
    
      return (
        <BlockStack gap="base">
          <Text fontWeight="bold">Product Customizer</Text>
          <Text>
            Customize product: {productId}
          </Text>
          {settings.show_preview && (
            <Text size="small">Preview enabled</Text>
          )}
          <InlineStack gap="base">
            <Button onPress={handleCustomize}>
              Apply Customization
            </Button>
          </InlineStack>
        </BlockStack>
      );
    }
    
    // Checkout UI Extension
    // [[extensions.targeting]]
    // target = "purchase.checkout.block.render"
    
    // extensions/checkout-ext/src/Checkout.tsx
    import {
      reactExtension,
      Banner,
      useCartLines,
      useTotalAmount,
    } from "@shopify/ui-extensions-react/checkout";
    
    export default reactExtension(
      "purchase.checkout.block.render",
      () => <CheckoutBanner />
    );
    
    function CheckoutBanner() {
      const cartLines = useCartLines();
      const total = useTotalAmount();
    
      if (total.amount > 100) {
        return (
          <Banner status="success">
            You qualify for free shipping!
          </Banner>
        );
      }
    
      return null;
    }
    
    ### Notes
    
    - Extensions run in sandboxed iframe
    - Use @shopify/ui-extensions-react for React
    - Limited APIs compared to full app
    - Deploy with 'shopify app deploy'
    
    ## Sharp Edges
    
    ### Webhook Must Respond Within 5 Seconds
    
    Severity: HIGH
    
    Situation: Receiving webhooks from Shopify
    
    Symptoms:
    Webhook deliveries marked as failed.
    "Your app didn't respond in time" in Shopify logs.
    Missing order/product updates.
    Webhooks retried repeatedly then cancelled.
    
    Why this breaks:
    Shopify expects a 2xx response within 5 seconds. If your app processes
    the webhook data before responding, you'll timeout.
    
    Shopify retries failed webhooks up to 19 times over 48 hours.
    After continued failures, webhooks may be cancelled entirely.
    
    Heavy processing (API calls, database operations) must happen
    after the response is sent.
    
    Recommended fix:
    
    ## Respond immediately, process asynchronously
    
    ```typescript
    // app/routes/webhooks.tsx
    export const action = async ({ request }: ActionFunctionArgs) => {
      const { topic, shop, payload } = await authenticate.webhook(request);
    
      // Queue for async processing
      await jobQueue.add("process-webhook", {
        topic,
        shop,
        payload,
      });
    
      // CRITICAL: Return 200 immediately
      return new Response(null, { status: 200 });
    };
    
    // Worker process handles the actual work
    // workers/webhook-processor.ts
    import { Worker } from "bullmq";
    
    const worker = new Worker("process-webhook", async (job) => {
      const { topic, shop, payload } = job.data;
    
      switch (topic) {
        case "ORDERS_CREATE":
          await processOrder(shop, payload);
          break;
        // ... other handlers
      }
    });
    ```
    
    ## For simple operations, be quick
    
    ```typescript
    // Simple database update is OK if fast
    export const action = async ({ request }: ActionFunctionArgs) => {
      const { topic, payload } = await authenticate.webhook(request);
    
      // Quick database update (< 1 second)
      await db.product.update({
        where: { shopifyId: payload.id },
        data: { title: payload.title },
      });
    
      return new Response(null, { status: 200 });
    };
    ```
    
    ## Monitor webhook performance
    
    ```typescript
    // Log response times
    const start = Date.now();
    
    await handleWebhook(payload);
    
    const duration = Date.now() - start;
    console.log(`Webhook processed in ${duration}ms`);
    
    // Alert if approaching timeout
    if (duration > 3000) {
      console.warn("Webhook processing taking too long!");
    }
    ```
    
    ### API Rate Limits Cause 429 Errors
    
    Severity: HIGH
    
    Situation: Making API calls to Shopify
    
    Symptoms:
    HTTP 429 Too Many Requests errors.
    "Throttled" responses.
    App becomes unresponsive.
    Operations fail silently or partially.
    
    Why this breaks:
    Shopify enforces strict rate limits:
    - REST: 2 requests per second per store
    - GraphQL: 1000 points per 60 seconds
    
    Exceeding limits causes immediate 429 errors.
    Continuous violations can result in temporary bans.
    
    Bulk operations count against limits.
    
    Recommended fix:
    
    ## Check rate limit headers
    
    ```typescript
    // REST API
    // X-Shopify-Shop-Api-Call-Limit: 39/40
    
    // GraphQL - check response extensions
    const response = await admin.graphql(`...`);
    const { data, extensions } = await response.json();
    
    const cost = extensions?.cost;
    // {
    //   "requestedQueryCost": 42,
    //   "actualQueryCost": 42,
    //   "throttleStatus": {
    //     "maximumAvailable": 1000,
    //     "currentlyAvailable": 958,
    //     "restoreRate": 50
    //   }
    // }
    ```
    
    ## Implement retry with exponential backoff
    
    ```typescript
    async function shopifyRequest(
      fn: () => Promise<Response>,
      maxRetries = 3
    ): Promise<Response> {
      let lastError: Error;
    
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
          const response = await fn();
    
          if (response.status === 429) {
            // Get retry-after header or default
            const retryAfter = parseInt(
              response.headers.get("Retry-After") || "2"
            );
            await sleep(retryAfter * 1000 * Math.pow(2, attempt));
            continue;
          }
    
          return response;
        } catch (error) {
          lastError = error as Error;
        }
      }
    
      throw lastError!;
    }
    ```
    
    ## Use bulk operations for large datasets
    
    ```typescript
    // Instead of 1000 individual calls, use bulk mutation
    const response = await admin.graphql(`
      mutation {
        bulkOperationRunMutation(
          mutation: "mutation($input: ProductInput!) {
            productUpdate(input: $input) { product { id } }
          }",
          stagedUploadPath: "..."
        ) {
          bulkOperation { id status }
          userErrors { message }
        }
      }
    `);
    ```
    
    ## Queue requests
    
    ```typescript
    import { RateLimiter } from "limiter";
    
    // 2 requests per second for REST
    const limiter = new RateLimiter({
      tokensPerInterval: 2,
      interval: "second",
    });
    
    async function rateLimitedRequest(fn: () => Promise<any>) {
      await limiter.removeTokens(1);
      return fn();
    }
    ```
    
    ### Protected Customer Data Requires Special Permission
    
    Severity: HIGH
    
    Situation: Accessing customer PII in webhooks or API
    
    Symptoms:
    Webhook deliveries fail for orders/customers.
    Customer data fields are null or empty.
    App works in development but fails in production.
    "Protected customer data access" errors.
    
    Why this breaks:
    Since April 2024, accessing protected customer data (PII) requires
    explicit approval from Shopify. This is separate from OAuth scopes.
    
    Protected data includes:
    - Customer names, emails, addresses
    - Order customer information
    - Subscription customer details
    
    Even with read_orders scope, you won't receive customer data
    in webhooks without protected data access.
    
    Recommended fix:
    
    ## Request protected customer data access
    
    1. Go to Partner Dashboard > App > API access
    2. Under "Protected customer data access"
    3. Request access for needed data types
    4. Justify your use case
    5. Wait for Shopify approval (can take days)
    
    ## Check your data access level
    
    ```typescript
    // Query your app's data access
    const response = await admin.graphql(`
      query {
        currentAppInstallation {
          accessScopes {
            handle
          }
        }
      }
    `);
    ```
    
    ## Handle missing data gracefully
    
    ```typescript
    // Webhook payload may have redacted fields
    async function processOrder(payload: any) {
      const customerEmail = payload.customer?.email;
    
      if (!customerEmail) {
        // Customer data not available
        // Either no protected access or data redacted
        console.log("Customer data not available");
        return;
      }
    
      await sendOrderConfirmation(customerEmail);
    }
    ```
    
    ## Use customer account API for direct access
    
    ```typescript
    // If customer is logged in, can access their data
    // through Customer Account API (different from Admin API)
    ```
    
    ### Duplicate Webhook Definitions Cause Conflicts
    
    Severity: MEDIUM
    
    Situation: Configuring webhooks in both TOML and code
    
    Symptoms:
    Duplicate webhook deliveries.
    Some webhooks fire twice.
    Webhook subscriptions fail to register.
    Unpredictable webhook behavior.
    
    Why this breaks:
    Shopify apps can define webhooks in two places:
    1. shopify.app.toml (declarative, recommended)
    2. afterAuth hook in code (imperative, legacy)
    
    If you define the same webhook in both places, you get:
    - Duplicate subscriptions
    - Race conditions during registration
    - Conflicts during app updates
    
    Recommended fix:
    
    ## Use TOML only (recommended)
    
    ```toml
    # shopify.app.toml
    [webhooks]
    api_version = "2024-10"
    
    [webhooks.subscriptions]
    topics = [
      "orders/create",
      "orders/updated",
      "products/create",
      "products/update",
      "app/uninstalled"
    ]
    uri = "/webhooks"
    ```
    
    ## Remove code-based registration
    
    ```typescript
    // DON'T do this if using TOML
    const shopify = shopifyApp({
      // ...
      hooks: {
        afterAuth: async ({ session }) => {
          // Remove webhook registration from here
          // Let TOML handle it
        },
      },
    });
    ```
    
    ## Deploy to apply TOML changes
    
    ```bash
    # Webhooks registered on deploy
    shopify app deploy
    ```
    
    ## Check current subscriptions
    
    ```typescript
    const response = await admin.graphql(`
      query {
        webhookSubscriptions(first: 50) {
          edges {
            node {
              id
              topic
              endpoint {
                ... on WebhookHttpEndpoint {
                  callbackUrl
                }
              }
            }
          }
        }
      }
    `);
    ```
    
    ### Webhook URL Trailing Slash Causes 404
    
    Severity: MEDIUM
    
    Situation: Setting up webhook endpoints
    
    Symptoms:
    Webhooks return 404 Not Found.
    Webhook delivery fails immediately.
    Works in local dev but fails in production.
    Logs show request to /webhooks/ not /webhooks.
    
    Why this breaks:
    Shopify automatically adds a trailing slash to webhook URLs.
    If your server doesn't handle both /webhooks and /webhooks/,
    the webhook will 404.
    
    Common with frameworks that are strict about trailing slashes.
    
    Recommended fix:
    
    ## Handle both URL formats
    
    ```typescript
    // Remix/React Router - both work by default
    // app/routes/webhooks.tsx handles /webhooks
    
    // Express - add middleware
    app.use((req, res, next) => {
      if (req.path.endsWith('/') && req.path.length > 1) {
        const query = req.url.slice(req.path.length);
        const safePath = req.path.slice(0, -1);
        res.redirect(301, safePath + query);
      }
      next();
    });
    ```
    
    ## Configure web server
    
    ```nginx
    # Nginx - strip trailing slashes
    location ~ ^(.+)/$ {
      return 301 $1;
    }
    
    # Or rewrite to handler
    location /webhooks {
      try_files $uri $uri/ @webhooks;
    }
    location @webhooks {
      proxy_pass http://app:3000/webhooks;
    }
    ```
    
    ## Test both formats
    
    ```bash
    # Test without slash
    curl -X POST https://your-app.com/webhooks
    
    # Test with slash
    curl -X POST https://your-app.com/webhooks/
    ```
    
    ### REST API Required Migration to GraphQL (April 2025)
    
    Severity: HIGH
    
    Situation: Building new public apps or maintaining existing
    
    Symptoms:
    App store submission rejected for REST API usage.
    Deprecation warnings in console.
    Some REST endpoints stop working.
    Missing features only in GraphQL.
    
    Why this breaks:
    As of October 2024, REST Admin API is legacy.
    Starting April 2025, new public apps MUST use GraphQL.
    
    REST endpoints will continue working for existing apps,
    but new features are GraphQL-only.
    
    Metafields, bulk operations, and many new features
    require GraphQL.
    
    Recommended fix:
    
    ## Use GraphQL for all new code
    
    ```typescript
    // REST (legacy)
    const response = await fetch(
      `https://${shop}/admin/api/2024-10/products.json`,
      {
        headers: { "X-Shopify-Access-Token": token },
      }
    );
    
    // GraphQL (recommended)
    const response = await admin.graphql(`
      query {
        products(first: 10) {
          edges {
            node {
              id
              title
            }
          }
        }
      }
    `);
    ```
    
    ## Migrate existing REST calls
    
    ```typescript
    // REST: GET /products/{id}.json
    // GraphQL equivalent:
    const response = await admin.graphql(`
      query GetProduct($id: ID!) {
        product(id: $id) {
          id
          title
          status
          variants(first: 10) {
            edges {
              node {
                id
                price
                inventoryQuantity
              }
            }
          }
        }
      }
    `, {
      variables: { id: `gid://shopify/Product/${productId}` },
    });
    ```
    
    ## Use GraphQL for webhooks too
    
    ```toml
    # shopify.app.toml
    [webhooks]
    api_version = "2024-10"  # Use latest GraphQL version
    ```
    
    ### App Bridge Required for Built for Shopify (July 2025)
    
    Severity: HIGH
    
    Situation: Building embedded Shopify apps
    
    Symptoms:
    App rejected from "Built for Shopify" program.
    App not appearing correctly in admin.
    Navigation and chrome issues.
    Warning about App Bridge version.
    
    Why this breaks:
    Effective July 2025, all apps seeking "Built for Shopify" status
    must use the latest version of App Bridge and be embedded.
    
    Apps using old App Bridge versions or not embedded will
    lose built for Shopify benefits (better placement, badges).
    
    Shopify now serves App Bridge and Polaris via unversioned
    script tags that auto-update.
    
    Recommended fix:
    
    ## Use latest App Bridge via script tag
    
    ```html
    <!-- Automatically stays up to date -->
    <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
    ```
    
    ## Use AppProvider in React
    
    ```typescript
    // app/routes/app.tsx
    import { AppProvider } from "@shopify/shopify-app-remix/react";
    
    export default function App() {
      return (
        <AppProvider isEmbeddedApp apiKey={apiKey}>
          <Outlet />
        </AppProvider>
      );
    }
    ```
    
    ## Enable embedded auth strategy
    
    ```typescript
    // shopify.server.ts
    const shopify = shopifyApp({
      // ...
      future: {
        unstable_newEmbeddedAuthStrategy: true,
      },
    });
    ```
    
    ## Check embedded status
    
    ```typescript
    import { useAppBridge } from "@shopify/app-bridge-react";
    
    function MyComponent() {
      const app = useAppBridge();
      const isEmbedded = app.hostOrigin !== window.location.origin;
    }
    ```
    
    ### Missing GDPR Webhooks Block App Store Approval
    
    Severity: HIGH
    
    Situation: Submitting app to Shopify App Store
    
    Symptoms:
    App submission rejected.
    "GDPR webhooks not implemented" error.
    Manual review fails for compliance.
    Data request webhooks not handled.
    
    Why this breaks:
    Shopify requires all apps to handle three GDPR webhooks:
    1. customers/data_request - Provide customer data
    2. customers/redact - Delete customer data
    3. shop/redact - Delete all shop data
    
    These are automatically subscribed when you create an app.
    You MUST implement handlers even if you don't store data.
    
    Recommended fix:
    
    ## Implement all GDPR handlers
    
    ```typescript
    // app/routes/webhooks.tsx
    export const action = async ({ request }: ActionFunctionArgs) => {
      const { topic, payload, shop } = await authenticate.webhook(request);
    
      switch (topic) {
        case "CUSTOMERS_DATA_REQUEST":
          await handleDataRequest(shop, payload);
          break;
    
        case "CUSTOMERS_REDACT":
          await handleCustomerRedact(shop, payload);
          break;
    
        case "SHOP_REDACT":
          await handleShopRedact(shop, payload);
          break;
      }
    
      return new Response(null, { status: 200 });
    };
    
    async function handleDataRequest(shop: string, payload: any) {
      const customerId = payload.customer.id;
    
      // Return customer data within 30 days
      // Usually send to data_request.destination_url
      const customerData = await db.customer.findUnique({
        where: { shopifyId: customerId, shop },
      });
    
      if (customerData) {
        // Send to provided URL or email
        await sendDataToMerchant(payload.data_request, customerData);
      }
    }
    
    async function handleCustomerRedact(shop: string, payload: any) {
      const customerId = payload.customer.id;
    
      // Delete customer's personal data
      await db.customer.deleteMany({
        where: { shopifyId: customerId, shop },
      });
    
      await db.order.updateMany({
        where: { customerId, shop },
        data: { customerEmail: null, customerName: null },
      });
    }
    
    async function handleShopRedact(shop: string, payload: any) {
      // Shop uninstalled 48+ hours ago
      // Delete ALL data for this shop
      await db.session.deleteMany({ where: { shop } });
      await db.customer.deleteMany({ where: { shop } });
      await db.order.deleteMany({ where: { shop } });
      await db.settings.deleteMany({ where: { shop } });
    }
    ```
    
    ## Even if you store nothing
    
    ```typescript
    // You must still respond 200
    case "CUSTOMERS_DATA_REQUEST":
    case "CUSTOMERS_REDACT":
    case "SHOP_REDACT":
      // No data stored, but must acknowledge
      console.log(`GDPR ${topic} for ${shop} - no data stored`);
      break;
    ```
    
    ## Validation Checks
    
    ### Hardcoded Shopify API Secret
    
    Severity: ERROR
    
    API secrets must never be hardcoded
    
    Message: Hardcoded Shopify API secret. Use environment variables.
    
    ### Hardcoded Shopify API Key
    
    Severity: ERROR
    
    API keys should use environment variables
    
    Message: Hardcoded Shopify API key. Use environment variables.
    
    ### Missing HMAC Verification
    
    Severity: ERROR
    
    Webhook endpoints must verify HMAC signature
    
    Message: Webhook handler without HMAC verification. Use authenticate.webhook().
    
    ### Synchronous Webhook Processing
    
    Severity: WARNING
    
    Webhook handlers should respond quickly
    
    Message: Multiple await calls in webhook handler. Consider async processing.
    
    ### Missing Webhook Response
    
    Severity: ERROR
    
    Webhooks must return 200 status
    
    Message: Webhook handler may not return proper response.
    
    ### Duplicate Webhook Registration
    
    Severity: WARNING
    
    Webhooks should be defined in TOML only
    
    Message: Code-based webhook registration. Define webhooks in shopify.app.toml.
    
    ### REST API Usage
    
    Severity: INFO
    
    REST API is deprecated, use GraphQL
    
    Message: REST API usage detected. Consider migrating to GraphQL.
    
    ### Missing Rate Limit Handling
    
    Severity: WARNING
    
    API calls should handle 429 responses
    
    Message: API call without rate limit handling. Implement retry logic.
    
    ### In-Memory Session Storage
    
    Severity: WARNING
    
    In-memory sessions don't scale
    
    Message: In-memory session storage. Use PrismaSessionStorage or similar.
    
    ### Missing Session Validation
    
    Severity: ERROR
    
    Routes should validate session
    
    Message: Loader without authentication. Use authenticate.admin(request).
    
    ## Collaboration
    
    ### Delegation Triggers
    
    - user needs payment processing -> stripe-integration (Shopify Payments or Stripe integration)
    - user needs custom authentication -> auth-specialist (Beyond Shopify OAuth)
    - user needs email/SMS notifications -> twilio-communications (Customer notifications outside Shopify)
    - user needs AI features -> llm-architect (Product descriptions, chatbots)
    - user needs serverless deployment -> aws-serverless (Lambda or Vercel deployment)
    
    ## When to Use
    - User mentions or implies: shopify app
    - User mentions or implies: shopify
    - User mentions or implies: embedded app
    - User mentions or implies: polaris
    - User mentions or implies: app bridge
    - User mentions or implies: shopify webhook
    
    ## Limitations
    - Use this skill only when the task clearly matches the scope described above.
    - Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
    - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related