All skills
Skillintermediate
App Development
- Building custom Shopify apps for merchants - Creating public apps for the Shopify App Store - Integrating third-party services with Shopify - Automating merchant workflows - Building embedded admin experiences
Claude Code Knowledge Pack7/10/2026
Overview
App Development
When to Use
- Building custom Shopify apps for merchants
- Creating public apps for the Shopify App Store
- Integrating third-party services with Shopify
- Automating merchant workflows
- Building embedded admin experiences
When NOT to Use
- Theme customization (use Liquid)
- Customer-facing storefronts (use Storefront API)
- Simple product displays (use Liquid or Storefront API)
- Checkout-only customizations (use Checkout Extensions)
App Architecture Overview
App Types
| Type | Use Case | Distribution |
|---|---|---|
| Custom App | Single merchant, private | Manual install |
| Public App | App Store listing | Shopify review |
| Sales Channel | Custom storefront | App Store |
| Embedded App | Admin integration | Either |
Modern Stack (2024+)
# Create new Shopify app with Remix template
npm create @shopify/app@latest
# Project structure
shopify-app/
├── app/
│ ├── routes/ # Remix routes
│ │ ├── app._index.tsx # Main app page
│ │ ├── app.products.tsx # Products page
│ │ └── webhooks.tsx # Webhook handlers
│ ├── shopify.server.ts # Shopify API client
│ └── db.server.ts # Database client
├── extensions/ # App extensions
├── prisma/ # Database schema
├── shopify.app.toml # App configuration
└── package.json
App Configuration
shopify.app.toml
# shopify.app.toml
scopes = "read_products,write_products,read_orders,write_orders,read_customers"
[access_scopes]
# Use optional scopes for granular permissions
optional = ["read_inventory", "write_inventory"]
[auth]
redirect_urls = [
"https://your-app.com/auth/callback",
"https://your-app.com/auth/shopify/callback"
]
[webhooks]
api_version = "2024-10"
[[webhooks.subscriptions]]
topics = ["products/create", "products/update", "products/delete"]
uri = "/webhooks"
[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks"
[app_proxy]
url = "https://your-app.com/api/proxy"
subpath = "apps"
prefix = "your-app"
[pos]
embedded = false
[build]
automatically_update_urls_on_dev = true
dev_store_url = "your-dev-store.myshopify.com"
[app]
name = "Your App Name"
handle = "your-app-handle"
OAuth Implementation
Authentication Flow
// app/shopify.server.ts
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!,
appUrl: process.env.SHOPIFY_APP_URL!,
scopes: process.env.SCOPES?.split(","),
apiVersion: ApiVersion.October24,
distribution: AppDistribution.AppStore,
sessionStorage: new PrismaSessionStorage(prisma),
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
ORDERS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session, admin }) => {
// Register webhooks after OAuth
shopify.registerWebhooks({ session });
// Perform post-install setup
await setupShop(session, admin);
},
},
});
async function setupShop(session: Session, admin: AdminApiContext) {
// Store merchant data
await prisma.shop.upsert({
where: { shopDomain: session.shop },
update: { accessToken: session.accessToken },
create: {
shopDomain: session.shop,
accessToken: session.accessToken!,
installedAt: new Date(),
},
});
}
Protected Routes
// app/routes/app._index.tsx
const { admin, session } = await authenticate.admin(request);
// Make Admin API requests
const response = await admin.graphql(`
query {
shop {
name
email
myshopifyDomain
plan {
displayName
}
}
products(first: 10) {
edges {
node {
id
title
status
totalInventory
}
}
}
}
`);
const data = await response.json();
return json({
shop: data.data.shop,
products: data.data.products.edges.map((edge: any) => edge.node),
});
}
const { shop, products } = useLoaderData<typeof loader>();
const rows = products.map((product: any) => [
product.title,
product.status,
product.totalInventory,
]);
return (
<Layout.Section>
</Layout.Section>
);
}
Admin API (GraphQL)
Products CRUD
// Create product
const CREATE_PRODUCT = `
mutation productCreate($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
variants(first: 10) {
edges {
node {
id
price
sku
}
}
}
}
userErrors {
field
message
}
}
}
`;
// Usage
const response = await admin.graphql(CREATE_PRODUCT, {
variables: {
input: {
title: "New Product",
descriptionHtml: "<p>Product description</p>",
vendor: "Your Brand",
productType: "T-Shirt",
tags: ["new", "featured"],
variants: [
{
price: "29.99",
sku: "SKU-001",
inventoryManagement: "SHOPIFY",
inventoryPolicy: "DENY",
options: ["Small", "Blue"],
},
{
price: "29.99",
sku: "SKU-002",
options: ["Medium", "Blue"],
},
],
options: ["Size", "Color"],
},
},
});
// Update product
const UPDATE_PRODUCT = `
mutation productUpdate($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
`;
// Bulk operations for large datasets
const BULK_MUTATION = `
mutation bulkOperationRunMutation($mutation: String!, $stagedUploadPath: String!) {
bulkOperationRunMutation(mutation: $mutation, stagedUploadPath: $stagedUploadPath) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
`;
Orders Management
// Fetch orders with fulfillment status
const GET_ORDERS = `
query getOrders($first: Int!, $query: String) {
orders(first: $first, query: $query, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
createdAt
displayFinancialStatus
displayFulfillmentStatus
totalPriceSet {
shopMoney {
amount
currencyCode
}
}
customer {
firstName
lastName
email
}
lineItems(first: 5) {
edges {
node {
title
quantity
variant {
id
sku
}
}
}
}
shippingAddress {
address1
city
province
country
zip
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
// Create fulfillment
const CREATE_FULFILLMENT = `
mutation fulfillmentCreate($fulfillment: FulfillmentInput!) {
fulfillmentCreate(fulfillment: $fulfillment) {
fulfillment {
id
status
trackingInfo {
number
url
}
}
userErrors {
field
message
}
}
}
`;
Metafields
// Set product metafields
const SET_METAFIELDS = `
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id
namespace
key
value
type
}
userErrors {
field
message
}
}
}
`;
// Usage
await admin.graphql(SET_METAFIELDS, {
variables: {
metafields: [
{
ownerId: "gid://shopify/Product/123456789",
namespace: "custom",
key: "care_instructions",
value: "Machine wash cold",
type: "single_line_text_field",
},
{
ownerId: "gid://shopify/Product/123456789",
namespace: "custom",
key: "features",
value: JSON.stringify(["Organic cotton", "Fair trade", "Eco-friendly"]),
type: "list.single_line_text_field",
},
],
},
});
// Read metafields
const GET_PRODUCT_METAFIELDS = `
query getProductMetafields($id: ID!) {
product(id: $id) {
metafields(first: 20) {
edges {
node {
id
namespace
key
value
type
}
}
}
}
}
`;
Webhook Handling
Webhook Route
// app/routes/webhooks.tsx
const { topic, shop, session, admin, payload } =
await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
switch (topic) {
case "APP_UNINSTALLED":
await handleAppUninstalled(shop);
break;
case "PRODUCTS_CREATE":
await handleProductCreate(shop, payload);
break;
case "PRODUCTS_UPDATE":
await handleProductUpdate(shop, payload);
break;
case "ORDERS_CREATE":
await handleOrderCreate(shop, payload, admin);
break;
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;
default:
console.log(`Unhandled webhook topic: ${topic}`);
}
return new Response("OK", { status: 200 });
}
async function handleAppUninstalled(shop: string) {
// Clean up shop data
await db.shop.delete({
where: { shopDomain: shop },
});
}
async function handleProductCreate(shop: string, payload: any) {
// Sync product to your database
await db.product.create({
data: {
shopDomain: shop,
shopifyId: payload.admin_graphql_api_id,
title: payload.title,
handle: payload.handle,
status: payload.status,
},
});
}
async function handleOrderCreate(shop: string, payload: any, admin: any) {
// Process new order
const order = {
id: payload.admin_graphql_api_id,
orderNumber: payload.order_number,
totalPrice: payload.total_price,
customer: payload.customer,
lineItems: payload.line_items,
};
// Example: Add order note
await admin.graphql(`
mutation addOrderNote($id: ID!, $note: String!) {
orderUpdate(input: { id: $id, note: $note }) {
order { id }
userErrors { field message }
}
}
`, {
variables: {
id: order.id,
note: "Processed by Your App",
},
});
}
// GDPR webhooks (required for public apps)
async function handleDataRequest(shop: string, payload: any) {
// Return customer data
const customerId = payload.customer.id;
// Gather and return all customer data
}
async function handleCustomerRedact(shop: string, payload: any) {
// Delete customer data
const customerId = payload.customer.id;
await db.customerData.deleteMany({
where: { shopDomain: shop, customerId: String(customerId) },
});
}
async function handleShopRedact(shop: string, payload: any) {
// Delete all shop data (48 hours after uninstall)
await db.shop.delete({ where: { shopDomain: shop } });
}
App Bridge 4.0
Setup
// app/root.tsx
const { apiKey } = useLoaderData<typeof loader>();
return (
<html>
<head>
</head>
<body>
</body>
</html>
);
}
App Bridge Actions
// Using App Bridge in components
function MyComponent() {
const app = useAppBridge();
const redirectToProduct = (productId: string) => {
const redirect = Redirect.create(app);
redirect.dispatch(Redirect.Action.ADMIN_PATH, {
path: `/products/${productId}`,
});
};
const openResourcePicker = async () => {
const selection = await app.resourcePicker({
type: "product",
multiple: true,
filter: {
variants: false,
archived: false,
},
});
if (selection) {
console.log("Selected products:", selection);
}
};
return (
Select Products
);
}
Toast Notifications
function SaveButton() {
const app = useAppBridge();
const handleSave = async () =>