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

TypeUse CaseDistribution
Custom AppSingle merchant, privateManual install
Public AppApp Store listingShopify review
Sales ChannelCustom storefrontApp Store
Embedded AppAdmin integrationEither

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 () =>