skills_all/shopify-app-dev/SKILL.md
Custom Shopify app development using Shopify CLI, app architecture, OAuth authentication, app extensions, admin UI, Hydrogen/Remix frameworks, and deployment. Use when creating Shopify apps, setting up Shopify CLI, building app extensions, implementing OAuth flows, creating admin UI components, working with Hydrogen or Remix, deploying to Cloudflare Workers, or integrating third-party services with Shopify stores.
npx skillsauth add activer007/ordinary-claude-skills shopify-app-devInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for building custom Shopify apps using Shopify CLI, modern frameworks, and best practices.
Invoke this skill when:
Install and configure Shopify CLI for app development.
Install Shopify CLI:
# Using npm
npm install -g @shopify/cli @shopify/app
# Using Homebrew (macOS)
brew tap shopify/shopify
brew install shopify-cli
# Verify installation
shopify version
Create New App:
# Create app with Node.js/React
shopify app init
# Choose template:
# - Remix (recommended)
# - Node.js + React
# - PHP
# - Ruby
# App structure created:
my-app/
├── app/ # Remix app routes
├── extensions/ # App extensions
├── shopify.app.toml # App configuration
├── package.json
└── README.md
App Configuration (shopify.app.toml):
# This file stores app configuration
name = "my-app"
client_id = "your-client-id"
application_url = "https://your-app.com"
embedded = true
[access_scopes]
# API access scopes
scopes = "write_products,read_orders,read_customers"
[auth]
redirect_urls = [
"https://your-app.com/auth/callback",
"https://your-app.com/auth/shopify/callback"
]
[webhooks]
api_version = "2025-10"
[[webhooks.subscriptions]]
topics = ["products/create", "products/update"]
uri = "/webhooks"
Start Development Server:
# Start dev server with tunneling
shopify app dev
# Server starts with:
# - Local development URL: http://localhost:3000
# - Public tunnel URL: https://random-subdomain.ngrok.io
# - App installed in development store
Deploy App:
# Deploy to production
shopify app deploy
# Generate app version and deploy extensions
Environment Variables (.env):
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SCOPES=write_products,read_orders
HOST=your-app-domain.com
SHOPIFY_APP_URL=https://your-app.com
DATABASE_URL=postgresql://...
Modern Shopify app using Remix framework.
app/routes/app._index.jsx (Home Page):
import { useLoaderData } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import {
Page,
Layout,
Card,
DataTable,
Button,
} from "@shopify/polaris";
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
// Fetch products using GraphQL
const response = await admin.graphql(`
query {
products(first: 10) {
edges {
node {
id
title
handle
status
}
}
}
}
`);
const { data } = await response.json();
return {
products: data.products.edges.map(e => e.node),
shop: session.shop,
};
}
export default function Index() {
const { products, shop } = useLoaderData();
const rows = products.map((product) => [
product.title,
product.handle,
product.status,
]);
return (
<Page title="Products">
<Layout>
<Layout.Section>
<Card>
<DataTable
columnContentTypes={["text", "text", "text"]}
headings={["Title", "Handle", "Status"]}
rows={rows}
/>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
app/routes/app.product.$id.jsx (Product Detail):
import { json } from "@remix-run/node";
import { useLoaderData, useSubmit } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import {
Page,
Layout,
Card,
Form,
FormLayout,
TextField,
Button,
} from "@shopify/polaris";
import { useState } from "react";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`
query GetProduct($id: ID!) {
product(id: $id) {
id
title
description
status
vendor
}
}
`, {
variables: { id: `gid://shopify/Product/${params.id}` },
});
const { data } = await response.json();
return json({ product: data.product });
}
export async function action({ request, params }) {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const title = formData.get("title");
const description = formData.get("description");
const response = await admin.graphql(`
mutation UpdateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
`, {
variables: {
input: {
id: `gid://shopify/Product/${params.id}`,
title,
description,
},
},
});
const { data } = await response.json();
if (data.productUpdate.userErrors.length > 0) {
return json({ errors: data.productUpdate.userErrors }, { status: 400 });
}
return json({ success: true });
}
export default function ProductDetail() {
const { product } = useLoaderData();
const submit = useSubmit();
const [title, setTitle] = useState(product.title);
const [description, setDescription] = useState(product.description);
const handleSubmit = () => {
const formData = new FormData();
formData.append("title", title);
formData.append("description", description);
submit(formData, { method: "post" });
};
return (
<Page title="Edit Product" backAction={{ url: "/app" }}>
<Layout>
<Layout.Section>
<Card>
<FormLayout>
<TextField
label="Title"
value={title}
onChange={setTitle}
autoComplete="off"
/>
<TextField
label="Description"
value={description}
onChange={setDescription}
multiline={4}
autoComplete="off"
/>
<Button primary onClick={handleSubmit}>
Save
</Button>
</FormLayout>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
Extend Shopify functionality with various extension types.
Admin Action Extension:
Create button in admin product page:
shopify app generate extension
# Choose: Admin action
# Name: Export Product
extensions/export-product/src/index.jsx:
import { extend, AdminAction } from "@shopify/admin-ui-extensions";
extend("Admin::Product::SubscriptionAction", (root, { data }) => {
const { id, title } = data.selected[0];
const button = root.createComponent(AdminAction, {
title: "Export Product",
onPress: async () => {
// Call your app API
const response = await fetch("/api/export", {
method: "POST",
body: JSON.stringify({ productId: id }),
headers: { "Content-Type": "application/json" },
});
if (response.ok) {
root.toast.show("Product exported successfully!");
} else {
root.toast.show("Export failed", { isError: true });
}
},
});
root.append(button);
});
Theme App Extension:
Add app block to themes:
shopify app generate extension
# Choose: Theme app extension
# Name: Product Reviews
extensions/product-reviews/blocks/reviews.liquid:
{% schema %}
{
"name": "Product Reviews",
"target": "section",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Customer Reviews"
},
{
"type": "range",
"id": "reviews_to_show",
"label": "Reviews to Show",
"min": 1,
"max": 10,
"default": 5
}
]
}
{% endschema %}
<div class="product-reviews">
<h2>{{ block.settings.heading }}</h2>
{% comment %}
Fetch reviews from your app API
{% endcomment %}
<div id="reviews-container" data-product-id="{{ product.id }}"></div>
</div>
<script>
// Fetch and render reviews
fetch(`/apps/reviews/api/reviews?product_id={{ product.id }}&limit={{ block.settings.reviews_to_show }}`)
.then(r => r.json())
.then(reviews => {
const container = document.getElementById('reviews-container');
container.innerHTML = reviews.map(review => `
<div class="review">
<div class="rating">${'⭐'.repeat(review.rating)}</div>
<h3>${review.title}</h3>
<p>${review.content}</p>
<p class="author">- ${review.author}</p>
</div>
`).join('');
});
</script>
{% stylesheet %}
.product-reviews {
padding: 2rem;
}
.review {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.rating {
color: #ffa500;
margin-bottom: 0.5rem;
}
{% endstylesheet %}
Handle Shopify events in your app.
app/routes/webhooks.jsx:
import { authenticate } from "../shopify.server";
import db from "../db.server";
export async function action({ request }) {
const { topic, shop, session, admin, payload } = await authenticate.webhook(request);
console.log(`Webhook received: ${topic} from ${shop}`);
switch (topic) {
case "APP_UNINSTALLED":
// Clean up app data
await db.session.deleteMany({ where: { shop } });
break;
case "PRODUCTS_CREATE":
// Handle new product
console.log("New product created:", payload.id, payload.title);
await handleProductCreated(payload);
break;
case "PRODUCTS_UPDATE":
// Handle product update
console.log("Product updated:", payload.id);
await handleProductUpdated(payload);
break;
case "ORDERS_CREATE":
// Handle new order
console.log("New order:", payload.id, payload.email);
await handleOrderCreated(payload);
break;
case "CUSTOMERS_CREATE":
// Handle new customer
await handleCustomerCreated(payload);
break;
default:
console.log("Unhandled webhook topic:", topic);
}
return new Response("OK", { status: 200 });
}
async function handleProductCreated(product) {
// Process new product
await db.product.create({
data: {
shopifyId: product.id,
title: product.title,
handle: product.handle,
},
});
}
async function handleOrderCreated(order) {
// Send email notification, update inventory, etc.
console.log(`Order ${order.id} received for ${order.email}`);
}
Register Webhooks (app/shopify.server.js):
import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
DeliveryMethod,
} from "@shopify/shopify-app-remix/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 SQLiteSessionStorage(),
distribution: AppDistribution.AppStore,
apiVersion: ApiVersion.October25,
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
ORDERS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
});
export default shopify;
export const authenticate = shopify.authenticate;
Create custom storefront routes that access your app.
Setup in Partner Dashboard:
Subpath prefix: apps
Subpath: reviews
Proxy URL: https://your-app.com/api/proxy
Result:
https://store.com/apps/reviews → proxies to → https://your-app.com/api/proxy
Handle Proxy Requests (app/routes/api.proxy.jsx):
import { json } from "@remix-run/node";
export async function loader({ request }) {
const url = new URL(request.url);
// Verify proxy request
const signature = url.searchParams.get("signature");
const shop = url.searchParams.get("shop");
if (!verifyProxySignature(signature, request)) {
return json({ error: "Invalid signature" }, { status: 401 });
}
// Handle different paths
const path = url.searchParams.get("path_prefix");
if (path === "/apps/reviews/product") {
const productId = url.searchParams.get("product_id");
const reviews = await getProductReviews(productId);
return json({ reviews });
}
return json({ message: "App Proxy" });
}
function verifyProxySignature(signature, request) {
// Verify HMAC signature
// Implementation depends on your setup
return true;
}
Use Shopify's design system for consistent admin UI.
Common Components:
import {
Page,
Layout,
Card,
Button,
TextField,
Select,
Checkbox,
Badge,
Banner,
DataTable,
Modal,
Toast,
Frame,
} from "@shopify/polaris";
export default function MyPage() {
return (
<Page
title="Settings"
primaryAction={{ content: "Save", onAction: handleSave }}
secondaryActions={[{ content: "Cancel", onAction: handleCancel }]}
>
<Layout>
<Layout.Section>
<Card title="General Settings" sectioned>
<TextField
label="App Name"
value={name}
onChange={setName}
/>
<Select
label="Status"
options={[
{ label: "Active", value: "active" },
{ label: "Draft", value: "draft" },
]}
value={status}
onChange={setStatus}
/>
<Checkbox
label="Enable notifications"
checked={notifications}
onChange={setNotifications}
/>
</Card>
</Layout.Section>
<Layout.Section secondary>
<Card title="Status" sectioned>
<Badge status="success">Active</Badge>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
Deploy Shopify apps to production.
Deploy to Cloudflare Workers:
wrangler.toml:
name = "shopify-app"
compatibility_date = "2025-11-10"
main = "build/index.js"
[vars]
SHOPIFY_API_KEY = "your_api_key"
[[kv_namespaces]]
binding = "SESSIONS"
id = "your_kv_namespace_id"
Deploy:
# Build app
npm run build
# Deploy to Cloudflare
wrangler deploy
Environment Secrets:
# Add secrets
wrangler secret put SHOPIFY_API_SECRET
wrangler secret put DATABASE_URL
# Create app
shopify app init
# Start development
shopify app dev
# Generate extension
shopify app generate extension
# Deploy app
shopify app deploy
# Configure webhooks
# Edit shopify.app.toml
tools
Generate typed TypeScript SDKs for AI agents to interact with MCP servers. Converts verbose JSON-RPC curl commands to clean function calls (docs.createDocument() vs curl). Auto-detects MCP tools from server modules, generates TypeScript types and client methods, creates runnable example scripts. Use when: building MCP-enabled applications, need typed programmatic access to MCP tools, want Claude Code to manage apps via scripts, eliminating manual JSON-RPC curl commands, validating MCP inputs/outputs, or creating reusable agent automation.
testing
Generate structured task lists from specs or requirements. IMPORTANT: After completing ANY spec via ExitSpecMode, ALWAYS ask the user: "Would you like me to generate a task list for this spec?" Use when user confirms or explicitly requests task generation from a plan/spec/PRD.
tools
Create compelling story-format summaries using UltraThink to find the best narrative framing. Support multiple formats - 3-part narrative, n-length with inline links, abridged 5-line, or comprehensive via Foundry MCP. USE WHEN user says 'create story explanation', 'narrative summary', 'explain as a story', or wants content in Daniel's conversational first-person voice.
testing
Navigate through the original three-world shamanic technology. Deploy when soul retrieval, power animal guidance, or journey between realms emerges. Deeply respectful of Tungus, Buryat, Yakut, Evenki traditions. Use for consciousness navigation, NOT cultural appropriation.