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";
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()
// Instead of 1000 individual calls, use bulk mutation
const response = await admin.graphql(`
mutation {
bulkOperationRunMutation(
mutation: "mutation($input: ProductInput!) {
productUpdate(input: $input) { product { id } }
}",
stagedUploadPath: "..."
) {
bulkOperation { id status }
userErrors { message }
}
}
`);
Queue requests
import { RateLimiter } from "limiter";
// 2 requests per second for REST
const limiter = new RateLimiter({
tokensPerInterval: 2,
interval: "second",
});
async function rateLimitedRequest(fn: () => Promise<any>) {
await limiter.removeTokens(1);
return fn();
}
Protected Customer Data Requires Special Permission
Severity: HIGH
Situation: Accessing customer PII in webhooks or API
Symptoms:
Webhook deliveries fail for orders/customers.
Customer data fields are null or empty.
App works in development but fails in production.
"Protected customer data access" errors.
Why this breaks:
Since April 2024, accessing protected customer data (PII) requires
explicit approval from Shopify. This is separate from OAuth scopes.
Protected data includes:
Customer names, emails, addresses
Order customer information
Subscription customer details
Even with read_orders scope, you won't receive customer data
in webhooks without protected data access.
// Webhook payload may have redacted fields
async function processOrder(payload: any) {
const customerEmail = payload.customer?.email;
if (!customerEmail) {
// Customer data not available
// Either no protected access or data redacted
console.log("Customer data not available");
return;
}
await sendOrderConfirmation(customerEmail);
}
Use customer account API for direct access
// If customer is logged in, can access their data
// through Customer Account API (different from Admin API)
Duplicate Webhook Definitions Cause Conflicts
Severity: MEDIUM
Situation: Configuring webhooks in both TOML and code
Symptoms:
Duplicate webhook deliveries.
Some webhooks fire twice.
Webhook subscriptions fail to register.
Unpredictable webhook behavior.
Why this breaks:
Shopify apps can define webhooks in two places:
shopify.app.toml (declarative, recommended)
afterAuth hook in code (imperative, legacy)
If you define the same webhook in both places, you get:
// DON'T do this if using TOML
const shopify = shopifyApp({
// ...
hooks: {
afterAuth: async ({ session }) => {
// Remove webhook registration from here
// Let TOML handle it
},
},
});
Deploy to apply TOML changes
# Webhooks registered on deploy
shopify app deploy
Symptoms:
Webhooks return 404 Not Found.
Webhook delivery fails immediately.
Works in local dev but fails in production.
Logs show request to /webhooks/ not /webhooks.
Why this breaks:
Shopify automatically adds a trailing slash to webhook URLs.
If your server doesn't handle both /webhooks and /webhooks/,
the webhook will 404.
Common with frameworks that are strict about trailing slashes.
# Test without slash
curl -X POST https://your-app.com/webhooks
# Test with slash
curl -X POST https://your-app.com/webhooks/
REST API Required Migration to GraphQL (April 2025)
Severity: HIGH
Situation: Building new public apps or maintaining existing
Symptoms:
App store submission rejected for REST API usage.
Deprecation warnings in console.
Some REST endpoints stop working.
Missing features only in GraphQL.
Why this breaks:
As of October 2024, REST Admin API is legacy.
Starting April 2025, new public apps MUST use GraphQL.
REST endpoints will continue working for existing apps,
but new features are GraphQL-only.
Metafields, bulk operations, and many new features
require GraphQL.
// REST: GET /products/{id}.json
// GraphQL equivalent:
const response = await admin.graphql(`
query GetProduct($id: ID!) {
product(id: $id) {
id
title
status
variants(first: 10) {
edges {
node {
id
price
inventoryQuantity
}
}
}
}
}
`, {
variables: { id: `gid://shopify/Product/${productId}` },
});
Use GraphQL for webhooks too
# shopify.app.toml
[webhooks]
api_version = "2024-10" # Use latest GraphQL version
App Bridge Required for Built for Shopify (July 2025)
Severity: HIGH
Situation: Building embedded Shopify apps
Symptoms:
App rejected from "Built for Shopify" program.
App not appearing correctly in admin.
Navigation and chrome issues.
Warning about App Bridge version.
Why this breaks:
Effective July 2025, all apps seeking "Built for Shopify" status
must use the latest version of App Bridge and be embedded.
Apps using old App Bridge versions or not embedded will
lose built for Shopify benefits (better placement, badges).
Shopify now serves App Bridge and Polaris via unversioned
script tags that auto-update.
Recommended fix:
Use latest App Bridge via script tag
<!-- Automatically stays up to date -->
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
import { useAppBridge } from "@shopify/app-bridge-react";
function MyComponent() {
const app = useAppBridge();
const isEmbedded = app.hostOrigin !== window.location.origin;
}
Missing GDPR Webhooks Block App Store Approval
Severity: HIGH
Situation: Submitting app to Shopify App Store
Symptoms:
App submission rejected.
"GDPR webhooks not implemented" error.
Manual review fails for compliance.
Data request webhooks not handled.
Why this breaks:
Shopify requires all apps to handle three GDPR webhooks:
customers/data_request - Provide customer data
customers/redact - Delete customer data
shop/redact - Delete all shop data
These are automatically subscribed when you create an app.
You MUST implement handlers even if you don't store data.
Recommended fix:
Implement all GDPR handlers
// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, payload, shop } = await authenticate.webhook(request);
switch (topic) {
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;
}
return new Response(null, { status: 200 });
};
async function handleDataRequest(shop: string, payload: any) {
const customerId = payload.customer.id;
// Return customer data within 30 days
// Usually send to data_request.destination_url
const customerData = await db.customer.findUnique({
where: { shopifyId: customerId, shop },
});
if (customerData) {
// Send to provided URL or email
await sendDataToMerchant(payload.data_request, customerData);
}
}
async function handleCustomerRedact(shop: string, payload: any) {
const customerId = payload.customer.id;
// Delete customer's personal data
await db.customer.deleteMany({
where: { shopifyId: customerId, shop },
});
await db.order.updateMany({
where: { customerId, shop },
data: { customerEmail: null, customerName: null },
});
}
async function handleShopRedact(shop: string, payload: any) {
// Shop uninstalled 48+ hours ago
// Delete ALL data for this shop
await db.session.deleteMany({ where: { shop } });
await db.customer.deleteMany({ where: { shop } });
await db.order.deleteMany({ where: { shop } });
await db.settings.deleteMany({ where: { shop } });
}
Even if you store nothing
// You must still respond 200
case "CUSTOMERS_DATA_REQUEST":
case "CUSTOMERS_REDACT":
case "SHOP_REDACT":
// No data stored, but must acknowledge
console.log(`GDPR ${topic} for ${shop} - no data stored`);
break;
Validation Checks
Hardcoded Shopify API Secret
Severity: ERROR
API secrets must never be hardcoded
Message: Hardcoded Shopify API secret. Use environment variables.
Hardcoded Shopify API Key
Severity: ERROR
API keys should use environment variables
Message: Hardcoded Shopify API key. Use environment variables.
Missing HMAC Verification
Severity: ERROR
Webhook endpoints must verify HMAC signature
Message: Webhook handler without HMAC verification. Use authenticate.webhook().
Synchronous Webhook Processing
Severity: WARNING
Webhook handlers should respond quickly
Message: Multiple await calls in webhook handler. Consider async processing.
Missing Webhook Response
Severity: ERROR
Webhooks must return 200 status
Message: Webhook handler may not return proper response.
Duplicate Webhook Registration
Severity: WARNING
Webhooks should be defined in TOML only
Message: Code-based webhook registration. Define webhooks in shopify.app.toml.
REST API Usage
Severity: INFO
REST API is deprecated, use GraphQL
Message: REST API usage detected. Consider migrating to GraphQL.
Missing Rate Limit Handling
Severity: WARNING
API calls should handle 429 responses
Message: API call without rate limit handling. Implement retry logic.
In-Memory Session Storage
Severity: WARNING
In-memory sessions don't scale
Message: In-memory session storage. Use PrismaSessionStorage or similar.
Missing Session Validation
Severity: ERROR
Routes should validate session
Message: Loader without authentication. Use authenticate.admin(request).
Collaboration
Delegation Triggers
user needs payment processing -> stripe-integration (Shopify Payments or Stripe integration)
user needs custom authentication -> auth-specialist (Beyond Shopify OAuth)
user needs email/SMS notifications -> twilio-communications (Customer notifications outside Shopify)
user needs AI features -> llm-architect (Product descriptions, chatbots)
user needs serverless deployment -> aws-serverless (Lambda or Vercel deployment)
When to Use
User mentions or implies: shopify app
User mentions or implies: shopify
User mentions or implies: embedded app
User mentions or implies: polaris
User mentions or implies: app bridge
User mentions or implies: shopify webhook
Limitations
Use this skill only when the task clearly matches the scope described above.
Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.