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.
What this skill does
# 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 querRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.