Vibeship-spawner-skills shopify-apps

Shopify Apps Integration Skill

install
source · Clone the upstream repo
git clone https://github.com/vibeforge1111/vibeship-spawner-skills
manifest: integrations/shopify-apps/skill.yaml
source content

Shopify Apps Integration Skill

Expert patterns for Shopify app development

version: 1.0.0 name: Shopify Apps id: shopify-apps category: integrations tags: [shopify, ecommerce, apps, embedded-apps, polaris, app-bridge]

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.

triggers:

  • "shopify app"
  • "shopify"
  • "embedded app"
  • "polaris"
  • "app bridge"
  • "shopify webhook"

anti_patterns:

  • name: REST API for New Apps description: REST API deprecated, GraphQL required for new public apps (April 2025) instead: Use GraphQL Admin API

  • name: Webhook Processing Before Response description: Processing webhooks before responding causes timeout instead: Respond immediately, process asynchronously

  • name: Polling Instead of Webhooks description: Wastes rate limits, slower than event-driven instead: Use webhooks for event notifications

  • name: Duplicate Webhook Definitions description: Defining webhooks in both TOML and code causes conflicts instead: Define webhooks in shopify.app.toml only

  • name: Ignoring Rate Limits description: Not handling 429 responses causes app failures instead: Implement exponential backoff and request queuing

handoffs:

  • situation: User needs payment processing delegate_to: stripe-integration context: Shopify Payments or custom checkout

  • situation: User needs frontend design system delegate_to: frontend context: Polaris for Shopify, Tailwind for custom

  • situation: User needs database design delegate_to: postgres-wizard context: Prisma with PostgreSQL common for Shopify apps

patterns:

  • name: React Router App Setup description: Modern Shopify app template with React Router when: 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"
  • name: Embedded App with App Bridge description: Render app embedded in Shopify Admin when: 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()"
  • name: Webhook Handling description: Secure webhook processing with HMAC verification when: 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()"
  • name: GraphQL Admin API description: Query and mutate shop data with GraphQL when: 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"
  • name: Billing API Integration description: Implement subscription billing for your app when: 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"
  • name: App Extension Development description: Extend Shopify checkout, admin, or storefront when: 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'"

best_practices:

  • practice: Define webhooks in TOML only why: Avoids duplicate subscriptions and conflicts implementation: |

    shopify.app.toml

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

  • practice: Use GraphQL over REST why: REST deprecated April 2025, GraphQL more efficient implementation: | const response = await admin.graphql(

        query { shop { name } }  
    );

  • practice: Implement proper rate limit handling why: Prevents 429 errors and app failures implementation: | // Check rate limit headers // X-Shopify-Shop-Api-Call-Limit: 40/40 // Implement exponential backoff

  • practice: Handle GDPR webhooks why: Required for App Store approval implementation: | // Handle customers/data_request, // customers/redact, shop/redact

  • practice: Use session storage with database why: In-memory storage doesn't scale implementation: | import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";