opencode-skills-collection 1.0.186 → 1.0.187

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +5 -1
  2. package/bundled-skills/3d-web-experience/SKILL.md +152 -37
  3. package/bundled-skills/agent-evaluation/SKILL.md +1088 -26
  4. package/bundled-skills/agent-memory-systems/SKILL.md +1037 -25
  5. package/bundled-skills/agent-tool-builder/SKILL.md +668 -16
  6. package/bundled-skills/ai-agents-architect/SKILL.md +271 -31
  7. package/bundled-skills/ai-product/SKILL.md +716 -26
  8. package/bundled-skills/ai-wrapper-product/SKILL.md +450 -44
  9. package/bundled-skills/algolia-search/SKILL.md +867 -15
  10. package/bundled-skills/autonomous-agents/SKILL.md +1033 -26
  11. package/bundled-skills/aws-serverless/SKILL.md +1046 -35
  12. package/bundled-skills/azure-functions/SKILL.md +1318 -19
  13. package/bundled-skills/browser-automation/SKILL.md +1065 -28
  14. package/bundled-skills/browser-extension-builder/SKILL.md +159 -32
  15. package/bundled-skills/bullmq-specialist/SKILL.md +347 -16
  16. package/bundled-skills/clerk-auth/SKILL.md +796 -15
  17. package/bundled-skills/computer-use-agents/SKILL.md +1870 -28
  18. package/bundled-skills/context-window-management/SKILL.md +271 -18
  19. package/bundled-skills/conversation-memory/SKILL.md +453 -24
  20. package/bundled-skills/crewai/SKILL.md +252 -46
  21. package/bundled-skills/discord-bot-architect/SKILL.md +1207 -34
  22. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  23. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  24. package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
  25. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  26. package/bundled-skills/docs/users/bundles.md +1 -1
  27. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  28. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  29. package/bundled-skills/docs/users/getting-started.md +1 -1
  30. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  31. package/bundled-skills/docs/users/usage.md +4 -4
  32. package/bundled-skills/docs/users/visual-guide.md +4 -4
  33. package/bundled-skills/email-systems/SKILL.md +646 -26
  34. package/bundled-skills/faf-expert/SKILL.md +221 -0
  35. package/bundled-skills/faf-wizard/SKILL.md +252 -0
  36. package/bundled-skills/file-uploads/SKILL.md +212 -11
  37. package/bundled-skills/firebase/SKILL.md +646 -16
  38. package/bundled-skills/gcp-cloud-run/SKILL.md +1117 -32
  39. package/bundled-skills/graphql/SKILL.md +1026 -27
  40. package/bundled-skills/hubspot-integration/SKILL.md +804 -19
  41. package/bundled-skills/idea-darwin/SKILL.md +120 -0
  42. package/bundled-skills/inngest/SKILL.md +431 -16
  43. package/bundled-skills/interactive-portfolio/SKILL.md +342 -44
  44. package/bundled-skills/langfuse/SKILL.md +296 -41
  45. package/bundled-skills/langgraph/SKILL.md +259 -50
  46. package/bundled-skills/micro-saas-launcher/SKILL.md +343 -44
  47. package/bundled-skills/neon-postgres/SKILL.md +572 -15
  48. package/bundled-skills/nextjs-supabase-auth/SKILL.md +269 -21
  49. package/bundled-skills/notion-template-business/SKILL.md +371 -44
  50. package/bundled-skills/personal-tool-builder/SKILL.md +537 -44
  51. package/bundled-skills/plaid-fintech/SKILL.md +825 -19
  52. package/bundled-skills/prompt-caching/SKILL.md +438 -25
  53. package/bundled-skills/rag-engineer/SKILL.md +271 -29
  54. package/bundled-skills/salesforce-development/SKILL.md +912 -19
  55. package/bundled-skills/satori/SKILL.md +54 -0
  56. package/bundled-skills/scroll-experience/SKILL.md +381 -44
  57. package/bundled-skills/segment-cdp/SKILL.md +817 -19
  58. package/bundled-skills/shopify-apps/SKILL.md +1475 -19
  59. package/bundled-skills/slack-bot-builder/SKILL.md +1162 -28
  60. package/bundled-skills/telegram-bot-builder/SKILL.md +152 -37
  61. package/bundled-skills/telegram-mini-app/SKILL.md +445 -44
  62. package/bundled-skills/trigger-dev/SKILL.md +916 -27
  63. package/bundled-skills/twilio-communications/SKILL.md +1310 -28
  64. package/bundled-skills/upstash-qstash/SKILL.md +898 -27
  65. package/bundled-skills/vercel-deployment/SKILL.md +637 -39
  66. package/bundled-skills/viral-generator-builder/SKILL.md +132 -37
  67. package/bundled-skills/voice-agents/SKILL.md +937 -27
  68. package/bundled-skills/voice-ai-development/SKILL.md +375 -46
  69. package/bundled-skills/workflow-automation/SKILL.md +982 -29
  70. package/bundled-skills/zapier-make-patterns/SKILL.md +772 -27
  71. package/package.json +1 -1
@@ -1,47 +1,1503 @@
1
1
  ---
2
2
  name: shopify-apps
3
- description: "Modern Shopify app template with React Router"
3
+ description: Expert patterns for Shopify app development including Remix/React
4
+ Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin
5
+ API, Polaris components, billing, and app extensions.
4
6
  risk: safe
5
- source: "vibeship-spawner-skills (Apache 2.0)"
6
- date_added: "2026-02-27"
7
+ source: vibeship-spawner-skills (Apache 2.0)
8
+ date_added: 2026-02-27
7
9
  ---
8
10
 
9
11
  # Shopify Apps
10
12
 
13
+ Expert patterns for Shopify app development including Remix/React Router apps,
14
+ embedded apps with App Bridge, webhook handling, GraphQL Admin API,
15
+ Polaris components, billing, and app extensions.
16
+
11
17
  ## Patterns
12
18
 
13
19
  ### React Router App Setup
14
20
 
15
21
  Modern Shopify app template with React Router
16
22
 
23
+ **When to use**: Starting a new Shopify app
24
+
25
+ ### Template
26
+
27
+ # Create new Shopify app with CLI
28
+ npm init @shopify/app@latest my-shopify-app
29
+
30
+ # Project structure
31
+ # my-shopify-app/
32
+ # ├── app/
33
+ # │ ├── routes/
34
+ # │ │ ├── app._index.tsx # Main app page
35
+ # │ │ ├── app.tsx # App layout with providers
36
+ # │ │ ├── auth.$.tsx # Auth callback
37
+ # │ │ └── webhooks.tsx # Webhook handler
38
+ # │ ├── shopify.server.ts # Server configuration
39
+ # │ └── root.tsx # Root layout
40
+ # ├── extensions/ # App extensions
41
+ # ├── shopify.app.toml # App configuration
42
+ # └── package.json
43
+
44
+ // shopify.app.toml
45
+ name = "my-shopify-app"
46
+ client_id = "your-client-id"
47
+ application_url = "https://your-app.example.com"
48
+
49
+ [access_scopes]
50
+ scopes = "read_products,write_products,read_orders"
51
+
52
+ [webhooks]
53
+ api_version = "2024-10"
54
+
55
+ [webhooks.subscriptions]
56
+ topics = ["orders/create", "products/update"]
57
+ uri = "/webhooks"
58
+
59
+ [auth]
60
+ redirect_urls = ["https://your-app.example.com/auth/callback"]
61
+
62
+ // app/shopify.server.ts
63
+ import "@shopify/shopify-app-remix/adapters/node";
64
+ import {
65
+ LATEST_API_VERSION,
66
+ shopifyApp,
67
+ DeliveryMethod,
68
+ } from "@shopify/shopify-app-remix/server";
69
+ import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
70
+ import prisma from "./db.server";
71
+
72
+ const shopify = shopifyApp({
73
+ apiKey: process.env.SHOPIFY_API_KEY!,
74
+ apiSecretKey: process.env.SHOPIFY_API_SECRET!,
75
+ scopes: process.env.SCOPES?.split(","),
76
+ appUrl: process.env.SHOPIFY_APP_URL!,
77
+ authPathPrefix: "/auth",
78
+ sessionStorage: new PrismaSessionStorage(prisma),
79
+ distribution: AppDistribution.AppStore,
80
+ future: {
81
+ unstable_newEmbeddedAuthStrategy: true,
82
+ },
83
+ ...(process.env.SHOP_CUSTOM_DOMAIN
84
+ ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
85
+ : {}),
86
+ });
87
+
88
+ export default shopify;
89
+ export const apiVersion = LATEST_API_VERSION;
90
+ export const authenticate = shopify.authenticate;
91
+ export const sessionStorage = shopify.sessionStorage;
92
+
93
+ ### Notes
94
+
95
+ - React Router replaced Remix as recommended template (late 2024)
96
+ - unstable_newEmbeddedAuthStrategy enabled by default for new apps
97
+ - Webhooks configured in shopify.app.toml, not code
98
+ - Run 'shopify app deploy' to apply configuration changes
99
+
17
100
  ### Embedded App with App Bridge
18
101
 
19
102
  Render app embedded in Shopify Admin
20
103
 
104
+ **When to use**: Building embedded admin app
105
+
106
+ ### Template
107
+
108
+ // app/routes/app.tsx - App layout with providers
109
+ import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
110
+ import { AppProvider } from "@shopify/shopify-app-remix/react";
111
+ import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
112
+
113
+ export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
114
+
115
+ export async function loader({ request }: LoaderFunctionArgs) {
116
+ await authenticate.admin(request);
117
+ return json({ apiKey: process.env.SHOPIFY_API_KEY! });
118
+ }
119
+
120
+ export default function App() {
121
+ const { apiKey } = useLoaderData<typeof loader>();
122
+
123
+ return (
124
+ <AppProvider isEmbeddedApp apiKey={apiKey}>
125
+ <ui-nav-menu>
126
+ <Link to="/app" rel="home">Home</Link>
127
+ <Link to="/app/products">Products</Link>
128
+ <Link to="/app/settings">Settings</Link>
129
+ </ui-nav-menu>
130
+ <Outlet />
131
+ </AppProvider>
132
+ );
133
+ }
134
+
135
+ export function ErrorBoundary() {
136
+ const error = useRouteError();
137
+ return (
138
+ <AppProvider isEmbeddedApp>
139
+ <Page>
140
+ <Card>
141
+ <Text as="p" variant="bodyMd">
142
+ Something went wrong. Please try again.
143
+ </Text>
144
+ </Card>
145
+ </Page>
146
+ </AppProvider>
147
+ );
148
+ }
149
+
150
+ // app/routes/app._index.tsx - Main app page
151
+ import {
152
+ Page,
153
+ Layout,
154
+ Card,
155
+ Text,
156
+ BlockStack,
157
+ Button,
158
+ } from "@shopify/polaris";
159
+ import { TitleBar } from "@shopify/app-bridge-react";
160
+
161
+ export async function loader({ request }: LoaderFunctionArgs) {
162
+ const { admin } = await authenticate.admin(request);
163
+
164
+ // GraphQL query
165
+ const response = await admin.graphql(`
166
+ query {
167
+ shop {
168
+ name
169
+ email
170
+ }
171
+ }
172
+ `);
173
+
174
+ const { data } = await response.json();
175
+ return json({ shop: data.shop });
176
+ }
177
+
178
+ export default function Index() {
179
+ const { shop } = useLoaderData<typeof loader>();
180
+
181
+ return (
182
+ <Page>
183
+ <TitleBar title="My Shopify App" />
184
+ <Layout>
185
+ <Layout.Section>
186
+ <Card>
187
+ <BlockStack gap="200">
188
+ <Text as="h2" variant="headingMd">
189
+ Welcome to {shop.name}!
190
+ </Text>
191
+ <Text as="p" variant="bodyMd">
192
+ Your app is now connected to this store.
193
+ </Text>
194
+ <Button variant="primary">
195
+ Get Started
196
+ </Button>
197
+ </BlockStack>
198
+ </Card>
199
+ </Layout.Section>
200
+ </Layout>
201
+ </Page>
202
+ );
203
+ }
204
+
205
+ ### Notes
206
+
207
+ - App Bridge required for Built for Shopify (July 2025)
208
+ - Polaris components match Shopify Admin design
209
+ - TitleBar and navigation from App Bridge
210
+ - Always authenticate requests with authenticate.admin()
211
+
21
212
  ### Webhook Handling
22
213
 
23
214
  Secure webhook processing with HMAC verification
24
215
 
25
- ## Anti-Patterns
216
+ **When to use**: Receiving Shopify webhooks
217
+
218
+ ### Template
219
+
220
+ // app/routes/webhooks.tsx
221
+ import type { ActionFunctionArgs } from "@remix-run/node";
222
+ import { authenticate } from "../shopify.server";
223
+ import db from "../db.server";
224
+
225
+ export const action = async ({ request }: ActionFunctionArgs) => {
226
+ // Authenticate webhook (verifies HMAC signature)
227
+ const { topic, shop, payload, admin } = await authenticate.webhook(request);
228
+
229
+ console.log(`Received ${topic} webhook for ${shop}`);
230
+
231
+ // Process based on topic
232
+ switch (topic) {
233
+ case "ORDERS_CREATE":
234
+ // Queue for async processing
235
+ await queueOrderProcessing(payload);
236
+ break;
237
+
238
+ case "PRODUCTS_UPDATE":
239
+ await handleProductUpdate(shop, payload);
240
+ break;
241
+
242
+ case "APP_UNINSTALLED":
243
+ // Clean up shop data
244
+ await db.session.deleteMany({ where: { shop } });
245
+ await db.shopData.delete({ where: { shop } });
246
+ break;
247
+
248
+ case "CUSTOMERS_DATA_REQUEST":
249
+ case "CUSTOMERS_REDACT":
250
+ case "SHOP_REDACT":
251
+ // GDPR webhooks - mandatory
252
+ await handleGDPRWebhook(topic, payload);
253
+ break;
254
+
255
+ default:
256
+ console.log(`Unhandled webhook topic: ${topic}`);
257
+ }
258
+
259
+ // CRITICAL: Return 200 immediately
260
+ // Shopify expects response within 5 seconds
261
+ return new Response(null, { status: 200 });
262
+ };
263
+
264
+ // Process asynchronously after responding
265
+ async function queueOrderProcessing(payload: any) {
266
+ // Use a job queue (BullMQ, etc.)
267
+ await jobQueue.add("process-order", {
268
+ orderId: payload.id,
269
+ orderData: payload,
270
+ });
271
+ }
272
+
273
+ async function handleProductUpdate(shop: string, payload: any) {
274
+ // Quick sync operation only
275
+ await db.product.upsert({
276
+ where: { shopifyId: payload.id },
277
+ update: {
278
+ title: payload.title,
279
+ updatedAt: new Date(),
280
+ },
281
+ create: {
282
+ shopifyId: payload.id,
283
+ shop,
284
+ title: payload.title,
285
+ },
286
+ });
287
+ }
288
+
289
+ async function handleGDPRWebhook(topic: string, payload: any) {
290
+ // GDPR compliance - required for all apps
291
+ switch (topic) {
292
+ case "CUSTOMERS_DATA_REQUEST":
293
+ // Return customer data within 30 days
294
+ break;
295
+ case "CUSTOMERS_REDACT":
296
+ // Delete customer data
297
+ break;
298
+ case "SHOP_REDACT":
299
+ // Delete all shop data (48 hours after uninstall)
300
+ break;
301
+ }
302
+ }
303
+
304
+ ### Notes
305
+
306
+ - Respond within 5 seconds or webhook fails
307
+ - Use job queues for heavy processing
308
+ - GDPR webhooks are mandatory for App Store
309
+ - HMAC verification handled by authenticate.webhook()
310
+
311
+ ### GraphQL Admin API
312
+
313
+ Query and mutate shop data with GraphQL
314
+
315
+ **When to use**: Interacting with Shopify Admin API
316
+
317
+ ### Template
318
+
319
+ // GraphQL queries with authenticated admin client
320
+ export async function loader({ request }: LoaderFunctionArgs) {
321
+ const { admin } = await authenticate.admin(request);
322
+
323
+ // Query products with pagination
324
+ const response = await admin.graphql(`
325
+ query GetProducts($first: Int!, $after: String) {
326
+ products(first: $first, after: $after) {
327
+ edges {
328
+ node {
329
+ id
330
+ title
331
+ status
332
+ totalInventory
333
+ priceRangeV2 {
334
+ minVariantPrice {
335
+ amount
336
+ currencyCode
337
+ }
338
+ }
339
+ images(first: 1) {
340
+ edges {
341
+ node {
342
+ url
343
+ altText
344
+ }
345
+ }
346
+ }
347
+ }
348
+ cursor
349
+ }
350
+ pageInfo {
351
+ hasNextPage
352
+ endCursor
353
+ }
354
+ }
355
+ }
356
+ `, {
357
+ variables: {
358
+ first: 10,
359
+ after: null,
360
+ },
361
+ });
362
+
363
+ const { data } = await response.json();
364
+ return json({ products: data.products });
365
+ }
366
+
367
+ // Mutations
368
+ export async function action({ request }: ActionFunctionArgs) {
369
+ const { admin } = await authenticate.admin(request);
370
+ const formData = await request.formData();
371
+ const productId = formData.get("productId");
372
+ const newTitle = formData.get("title");
373
+
374
+ const response = await admin.graphql(`
375
+ mutation UpdateProduct($input: ProductInput!) {
376
+ productUpdate(input: $input) {
377
+ product {
378
+ id
379
+ title
380
+ }
381
+ userErrors {
382
+ field
383
+ message
384
+ }
385
+ }
386
+ }
387
+ `, {
388
+ variables: {
389
+ input: {
390
+ id: productId,
391
+ title: newTitle,
392
+ },
393
+ },
394
+ });
395
+
396
+ const { data } = await response.json();
397
+
398
+ if (data.productUpdate.userErrors.length > 0) {
399
+ return json({
400
+ errors: data.productUpdate.userErrors,
401
+ }, { status: 400 });
402
+ }
403
+
404
+ return json({ product: data.productUpdate.product });
405
+ }
406
+
407
+ // Bulk operations for large datasets
408
+ async function bulkUpdateProducts(admin: AdminApiContext) {
409
+ // Create bulk operation
410
+ const response = await admin.graphql(`
411
+ mutation {
412
+ bulkOperationRunMutation(
413
+ mutation: "mutation call($input: ProductInput!) {
414
+ productUpdate(input: $input) { product { id } }
415
+ }",
416
+ stagedUploadPath: "path-to-staged-upload"
417
+ ) {
418
+ bulkOperation {
419
+ id
420
+ status
421
+ }
422
+ userErrors {
423
+ message
424
+ }
425
+ }
426
+ }
427
+ `);
428
+
429
+ // Poll for completion or use webhook
430
+ // BULK_OPERATIONS_FINISH webhook
431
+ }
432
+
433
+ ### Notes
434
+
435
+ - GraphQL required for new public apps (April 2025)
436
+ - Rate limit: 1000 points per 60 seconds
437
+ - Use bulk operations for >250 items
438
+ - Direct API access available from App Bridge
439
+
440
+ ### Billing API Integration
441
+
442
+ Implement subscription billing for your app
443
+
444
+ **When to use**: Monetizing Shopify app
445
+
446
+ ### Template
447
+
448
+ // app/routes/app.billing.tsx
449
+ import { json, redirect } from "@remix-run/node";
450
+ import { Page, Card, Button, BlockStack, Text } from "@shopify/polaris";
451
+ import { authenticate } from "../shopify.server";
452
+
453
+ const PLANS = {
454
+ basic: {
455
+ name: "Basic",
456
+ amount: 9.99,
457
+ currencyCode: "USD",
458
+ interval: "EVERY_30_DAYS",
459
+ },
460
+ pro: {
461
+ name: "Pro",
462
+ amount: 29.99,
463
+ currencyCode: "USD",
464
+ interval: "EVERY_30_DAYS",
465
+ },
466
+ };
467
+
468
+ export async function loader({ request }: LoaderFunctionArgs) {
469
+ const { admin, billing } = await authenticate.admin(request);
470
+
471
+ // Check current subscription
472
+ const response = await admin.graphql(`
473
+ query {
474
+ currentAppInstallation {
475
+ activeSubscriptions {
476
+ id
477
+ name
478
+ status
479
+ lineItems {
480
+ plan {
481
+ pricingDetails {
482
+ ... on AppRecurringPricing {
483
+ price {
484
+ amount
485
+ currencyCode
486
+ }
487
+ interval
488
+ }
489
+ }
490
+ }
491
+ }
492
+ }
493
+ }
494
+ }
495
+ `);
496
+
497
+ const { data } = await response.json();
498
+ return json({
499
+ subscription: data.currentAppInstallation.activeSubscriptions[0],
500
+ });
501
+ }
502
+
503
+ export async function action({ request }: ActionFunctionArgs) {
504
+ const { admin, session } = await authenticate.admin(request);
505
+ const formData = await request.formData();
506
+ const planKey = formData.get("plan") as keyof typeof PLANS;
507
+ const plan = PLANS[planKey];
508
+
509
+ // Create subscription charge
510
+ const response = await admin.graphql(`
511
+ mutation CreateSubscription($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $test: Boolean) {
512
+ appSubscriptionCreate(
513
+ name: $name
514
+ lineItems: $lineItems
515
+ returnUrl: $returnUrl
516
+ test: $test
517
+ ) {
518
+ appSubscription {
519
+ id
520
+ status
521
+ }
522
+ confirmationUrl
523
+ userErrors {
524
+ field
525
+ message
526
+ }
527
+ }
528
+ }
529
+ `, {
530
+ variables: {
531
+ name: plan.name,
532
+ lineItems: [
533
+ {
534
+ plan: {
535
+ appRecurringPricingDetails: {
536
+ price: {
537
+ amount: plan.amount,
538
+ currencyCode: plan.currencyCode,
539
+ },
540
+ interval: plan.interval,
541
+ },
542
+ },
543
+ },
544
+ ],
545
+ returnUrl: `https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}`,
546
+ test: process.env.NODE_ENV !== "production",
547
+ },
548
+ });
549
+
550
+ const { data } = await response.json();
551
+
552
+ if (data.appSubscriptionCreate.userErrors.length > 0) {
553
+ return json({
554
+ errors: data.appSubscriptionCreate.userErrors,
555
+ }, { status: 400 });
556
+ }
557
+
558
+ // Redirect merchant to approve charge
559
+ return redirect(data.appSubscriptionCreate.confirmationUrl);
560
+ }
561
+
562
+ export default function Billing() {
563
+ const { subscription } = useLoaderData<typeof loader>();
564
+ const submit = useSubmit();
565
+
566
+ return (
567
+ <Page title="Billing">
568
+ <Card>
569
+ {subscription ? (
570
+ <BlockStack gap="200">
571
+ <Text as="p" variant="bodyMd">
572
+ Current plan: {subscription.name}
573
+ </Text>
574
+ <Text as="p" variant="bodyMd">
575
+ Status: {subscription.status}
576
+ </Text>
577
+ </BlockStack>
578
+ ) : (
579
+ <BlockStack gap="400">
580
+ <Text as="h2" variant="headingMd">
581
+ Choose a Plan
582
+ </Text>
583
+ <Button onClick={() => submit({ plan: "basic" }, { method: "post" })}>
584
+ Basic - $9.99/month
585
+ </Button>
586
+ <Button onClick={() => submit({ plan: "pro" }, { method: "post" })}>
587
+ Pro - $29.99/month
588
+ </Button>
589
+ </BlockStack>
590
+ )}
591
+ </Card>
592
+ </Page>
593
+ );
594
+ }
595
+
596
+ ### Notes
597
+
598
+ - Use test: true for development stores
599
+ - Merchant must approve subscription
600
+ - One recurring + one usage charge per app max
601
+ - 30-day billing cycle for recurring charges
602
+
603
+ ### App Extension Development
604
+
605
+ Extend Shopify checkout, admin, or storefront
606
+
607
+ **When to use**: Building app extensions
608
+
609
+ ### Template
610
+
611
+ # shopify.extension.toml (in extensions/my-extension/)
612
+ api_version = "2024-10"
613
+
614
+ [[extensions]]
615
+ type = "ui_extension"
616
+ name = "Product Customizer"
617
+ handle = "product-customizer"
618
+
619
+ [[extensions.targeting]]
620
+ target = "admin.product-details.block.render"
621
+ module = "./src/AdminBlock.tsx"
622
+
623
+ [extensions.capabilities]
624
+ api_access = true
625
+
626
+ [extensions.settings]
627
+ [[extensions.settings.fields]]
628
+ key = "show_preview"
629
+ type = "boolean"
630
+ name = "Show Preview"
631
+
632
+ // extensions/my-extension/src/AdminBlock.tsx
633
+ import {
634
+ reactExtension,
635
+ useApi,
636
+ useSettings,
637
+ BlockStack,
638
+ Text,
639
+ Button,
640
+ InlineStack,
641
+ } from "@shopify/ui-extensions-react/admin";
642
+
643
+ export default reactExtension(
644
+ "admin.product-details.block.render",
645
+ () => <ProductCustomizer />
646
+ );
647
+
648
+ function ProductCustomizer() {
649
+ const { data, extension } = useApi<"admin.product-details.block.render">();
650
+ const settings = useSettings();
651
+
652
+ const productId = data?.selected?.[0]?.id;
653
+
654
+ const handleCustomize = async () => {
655
+ // API calls from extension
656
+ const result = await fetch("/api/customize", {
657
+ method: "POST",
658
+ body: JSON.stringify({ productId }),
659
+ });
660
+ };
661
+
662
+ return (
663
+ <BlockStack gap="base">
664
+ <Text fontWeight="bold">Product Customizer</Text>
665
+ <Text>
666
+ Customize product: {productId}
667
+ </Text>
668
+ {settings.show_preview && (
669
+ <Text size="small">Preview enabled</Text>
670
+ )}
671
+ <InlineStack gap="base">
672
+ <Button onPress={handleCustomize}>
673
+ Apply Customization
674
+ </Button>
675
+ </InlineStack>
676
+ </BlockStack>
677
+ );
678
+ }
679
+
680
+ // Checkout UI Extension
681
+ // [[extensions.targeting]]
682
+ // target = "purchase.checkout.block.render"
683
+
684
+ // extensions/checkout-ext/src/Checkout.tsx
685
+ import {
686
+ reactExtension,
687
+ Banner,
688
+ useCartLines,
689
+ useTotalAmount,
690
+ } from "@shopify/ui-extensions-react/checkout";
691
+
692
+ export default reactExtension(
693
+ "purchase.checkout.block.render",
694
+ () => <CheckoutBanner />
695
+ );
696
+
697
+ function CheckoutBanner() {
698
+ const cartLines = useCartLines();
699
+ const total = useTotalAmount();
700
+
701
+ if (total.amount > 100) {
702
+ return (
703
+ <Banner status="success">
704
+ You qualify for free shipping!
705
+ </Banner>
706
+ );
707
+ }
708
+
709
+ return null;
710
+ }
711
+
712
+ ### Notes
713
+
714
+ - Extensions run in sandboxed iframe
715
+ - Use @shopify/ui-extensions-react for React
716
+ - Limited APIs compared to full app
717
+ - Deploy with 'shopify app deploy'
718
+
719
+ ## Sharp Edges
720
+
721
+ ### Webhook Must Respond Within 5 Seconds
722
+
723
+ Severity: HIGH
724
+
725
+ Situation: Receiving webhooks from Shopify
726
+
727
+ Symptoms:
728
+ Webhook deliveries marked as failed.
729
+ "Your app didn't respond in time" in Shopify logs.
730
+ Missing order/product updates.
731
+ Webhooks retried repeatedly then cancelled.
732
+
733
+ Why this breaks:
734
+ Shopify expects a 2xx response within 5 seconds. If your app processes
735
+ the webhook data before responding, you'll timeout.
736
+
737
+ Shopify retries failed webhooks up to 19 times over 48 hours.
738
+ After continued failures, webhooks may be cancelled entirely.
739
+
740
+ Heavy processing (API calls, database operations) must happen
741
+ after the response is sent.
742
+
743
+ Recommended fix:
744
+
745
+ ## Respond immediately, process asynchronously
746
+
747
+ ```typescript
748
+ // app/routes/webhooks.tsx
749
+ export const action = async ({ request }: ActionFunctionArgs) => {
750
+ const { topic, shop, payload } = await authenticate.webhook(request);
751
+
752
+ // Queue for async processing
753
+ await jobQueue.add("process-webhook", {
754
+ topic,
755
+ shop,
756
+ payload,
757
+ });
758
+
759
+ // CRITICAL: Return 200 immediately
760
+ return new Response(null, { status: 200 });
761
+ };
762
+
763
+ // Worker process handles the actual work
764
+ // workers/webhook-processor.ts
765
+ import { Worker } from "bullmq";
766
+
767
+ const worker = new Worker("process-webhook", async (job) => {
768
+ const { topic, shop, payload } = job.data;
769
+
770
+ switch (topic) {
771
+ case "ORDERS_CREATE":
772
+ await processOrder(shop, payload);
773
+ break;
774
+ // ... other handlers
775
+ }
776
+ });
777
+ ```
778
+
779
+ ## For simple operations, be quick
780
+
781
+ ```typescript
782
+ // Simple database update is OK if fast
783
+ export const action = async ({ request }: ActionFunctionArgs) => {
784
+ const { topic, payload } = await authenticate.webhook(request);
785
+
786
+ // Quick database update (< 1 second)
787
+ await db.product.update({
788
+ where: { shopifyId: payload.id },
789
+ data: { title: payload.title },
790
+ });
791
+
792
+ return new Response(null, { status: 200 });
793
+ };
794
+ ```
795
+
796
+ ## Monitor webhook performance
797
+
798
+ ```typescript
799
+ // Log response times
800
+ const start = Date.now();
801
+
802
+ await handleWebhook(payload);
803
+
804
+ const duration = Date.now() - start;
805
+ console.log(`Webhook processed in ${duration}ms`);
806
+
807
+ // Alert if approaching timeout
808
+ if (duration > 3000) {
809
+ console.warn("Webhook processing taking too long!");
810
+ }
811
+ ```
812
+
813
+ ### API Rate Limits Cause 429 Errors
814
+
815
+ Severity: HIGH
816
+
817
+ Situation: Making API calls to Shopify
818
+
819
+ Symptoms:
820
+ HTTP 429 Too Many Requests errors.
821
+ "Throttled" responses.
822
+ App becomes unresponsive.
823
+ Operations fail silently or partially.
824
+
825
+ Why this breaks:
826
+ Shopify enforces strict rate limits:
827
+ - REST: 2 requests per second per store
828
+ - GraphQL: 1000 points per 60 seconds
829
+
830
+ Exceeding limits causes immediate 429 errors.
831
+ Continuous violations can result in temporary bans.
832
+
833
+ Bulk operations count against limits.
834
+
835
+ Recommended fix:
836
+
837
+ ## Check rate limit headers
838
+
839
+ ```typescript
840
+ // REST API
841
+ // X-Shopify-Shop-Api-Call-Limit: 39/40
842
+
843
+ // GraphQL - check response extensions
844
+ const response = await admin.graphql(`...`);
845
+ const { data, extensions } = await response.json();
846
+
847
+ const cost = extensions?.cost;
848
+ // {
849
+ // "requestedQueryCost": 42,
850
+ // "actualQueryCost": 42,
851
+ // "throttleStatus": {
852
+ // "maximumAvailable": 1000,
853
+ // "currentlyAvailable": 958,
854
+ // "restoreRate": 50
855
+ // }
856
+ // }
857
+ ```
858
+
859
+ ## Implement retry with exponential backoff
860
+
861
+ ```typescript
862
+ async function shopifyRequest(
863
+ fn: () => Promise<Response>,
864
+ maxRetries = 3
865
+ ): Promise<Response> {
866
+ let lastError: Error;
867
+
868
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
869
+ try {
870
+ const response = await fn();
871
+
872
+ if (response.status === 429) {
873
+ // Get retry-after header or default
874
+ const retryAfter = parseInt(
875
+ response.headers.get("Retry-After") || "2"
876
+ );
877
+ await sleep(retryAfter * 1000 * Math.pow(2, attempt));
878
+ continue;
879
+ }
880
+
881
+ return response;
882
+ } catch (error) {
883
+ lastError = error as Error;
884
+ }
885
+ }
886
+
887
+ throw lastError!;
888
+ }
889
+ ```
890
+
891
+ ## Use bulk operations for large datasets
892
+
893
+ ```typescript
894
+ // Instead of 1000 individual calls, use bulk mutation
895
+ const response = await admin.graphql(`
896
+ mutation {
897
+ bulkOperationRunMutation(
898
+ mutation: "mutation($input: ProductInput!) {
899
+ productUpdate(input: $input) { product { id } }
900
+ }",
901
+ stagedUploadPath: "..."
902
+ ) {
903
+ bulkOperation { id status }
904
+ userErrors { message }
905
+ }
906
+ }
907
+ `);
908
+ ```
909
+
910
+ ## Queue requests
26
911
 
27
- ### ❌ REST API for New Apps
912
+ ```typescript
913
+ import { RateLimiter } from "limiter";
28
914
 
29
- ### Webhook Processing Before Response
915
+ // 2 requests per second for REST
916
+ const limiter = new RateLimiter({
917
+ tokensPerInterval: 2,
918
+ interval: "second",
919
+ });
30
920
 
31
- ### Polling Instead of Webhooks
921
+ async function rateLimitedRequest(fn: () => Promise<any>) {
922
+ await limiter.removeTokens(1);
923
+ return fn();
924
+ }
925
+ ```
32
926
 
33
- ## ⚠️ Sharp Edges
927
+ ### Protected Customer Data Requires Special Permission
34
928
 
35
- | Issue | Severity | Solution |
36
- |-------|----------|----------|
37
- | Issue | high | ## Respond immediately, process asynchronously |
38
- | Issue | high | ## Check rate limit headers |
39
- | Issue | high | ## Request protected customer data access |
40
- | Issue | medium | ## Use TOML only (recommended) |
41
- | Issue | medium | ## Handle both URL formats |
42
- | Issue | high | ## Use GraphQL for all new code |
43
- | Issue | high | ## Use latest App Bridge via script tag |
44
- | Issue | high | ## Implement all GDPR handlers |
929
+ Severity: HIGH
930
+
931
+ Situation: Accessing customer PII in webhooks or API
932
+
933
+ Symptoms:
934
+ Webhook deliveries fail for orders/customers.
935
+ Customer data fields are null or empty.
936
+ App works in development but fails in production.
937
+ "Protected customer data access" errors.
938
+
939
+ Why this breaks:
940
+ Since April 2024, accessing protected customer data (PII) requires
941
+ explicit approval from Shopify. This is separate from OAuth scopes.
942
+
943
+ Protected data includes:
944
+ - Customer names, emails, addresses
945
+ - Order customer information
946
+ - Subscription customer details
947
+
948
+ Even with read_orders scope, you won't receive customer data
949
+ in webhooks without protected data access.
950
+
951
+ Recommended fix:
952
+
953
+ ## Request protected customer data access
954
+
955
+ 1. Go to Partner Dashboard > App > API access
956
+ 2. Under "Protected customer data access"
957
+ 3. Request access for needed data types
958
+ 4. Justify your use case
959
+ 5. Wait for Shopify approval (can take days)
960
+
961
+ ## Check your data access level
962
+
963
+ ```typescript
964
+ // Query your app's data access
965
+ const response = await admin.graphql(`
966
+ query {
967
+ currentAppInstallation {
968
+ accessScopes {
969
+ handle
970
+ }
971
+ }
972
+ }
973
+ `);
974
+ ```
975
+
976
+ ## Handle missing data gracefully
977
+
978
+ ```typescript
979
+ // Webhook payload may have redacted fields
980
+ async function processOrder(payload: any) {
981
+ const customerEmail = payload.customer?.email;
982
+
983
+ if (!customerEmail) {
984
+ // Customer data not available
985
+ // Either no protected access or data redacted
986
+ console.log("Customer data not available");
987
+ return;
988
+ }
989
+
990
+ await sendOrderConfirmation(customerEmail);
991
+ }
992
+ ```
993
+
994
+ ## Use customer account API for direct access
995
+
996
+ ```typescript
997
+ // If customer is logged in, can access their data
998
+ // through Customer Account API (different from Admin API)
999
+ ```
1000
+
1001
+ ### Duplicate Webhook Definitions Cause Conflicts
1002
+
1003
+ Severity: MEDIUM
1004
+
1005
+ Situation: Configuring webhooks in both TOML and code
1006
+
1007
+ Symptoms:
1008
+ Duplicate webhook deliveries.
1009
+ Some webhooks fire twice.
1010
+ Webhook subscriptions fail to register.
1011
+ Unpredictable webhook behavior.
1012
+
1013
+ Why this breaks:
1014
+ Shopify apps can define webhooks in two places:
1015
+ 1. shopify.app.toml (declarative, recommended)
1016
+ 2. afterAuth hook in code (imperative, legacy)
1017
+
1018
+ If you define the same webhook in both places, you get:
1019
+ - Duplicate subscriptions
1020
+ - Race conditions during registration
1021
+ - Conflicts during app updates
1022
+
1023
+ Recommended fix:
1024
+
1025
+ ## Use TOML only (recommended)
1026
+
1027
+ ```toml
1028
+ # shopify.app.toml
1029
+ [webhooks]
1030
+ api_version = "2024-10"
1031
+
1032
+ [webhooks.subscriptions]
1033
+ topics = [
1034
+ "orders/create",
1035
+ "orders/updated",
1036
+ "products/create",
1037
+ "products/update",
1038
+ "app/uninstalled"
1039
+ ]
1040
+ uri = "/webhooks"
1041
+ ```
1042
+
1043
+ ## Remove code-based registration
1044
+
1045
+ ```typescript
1046
+ // DON'T do this if using TOML
1047
+ const shopify = shopifyApp({
1048
+ // ...
1049
+ hooks: {
1050
+ afterAuth: async ({ session }) => {
1051
+ // Remove webhook registration from here
1052
+ // Let TOML handle it
1053
+ },
1054
+ },
1055
+ });
1056
+ ```
1057
+
1058
+ ## Deploy to apply TOML changes
1059
+
1060
+ ```bash
1061
+ # Webhooks registered on deploy
1062
+ shopify app deploy
1063
+ ```
1064
+
1065
+ ## Check current subscriptions
1066
+
1067
+ ```typescript
1068
+ const response = await admin.graphql(`
1069
+ query {
1070
+ webhookSubscriptions(first: 50) {
1071
+ edges {
1072
+ node {
1073
+ id
1074
+ topic
1075
+ endpoint {
1076
+ ... on WebhookHttpEndpoint {
1077
+ callbackUrl
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ `);
1085
+ ```
1086
+
1087
+ ### Webhook URL Trailing Slash Causes 404
1088
+
1089
+ Severity: MEDIUM
1090
+
1091
+ Situation: Setting up webhook endpoints
1092
+
1093
+ Symptoms:
1094
+ Webhooks return 404 Not Found.
1095
+ Webhook delivery fails immediately.
1096
+ Works in local dev but fails in production.
1097
+ Logs show request to /webhooks/ not /webhooks.
1098
+
1099
+ Why this breaks:
1100
+ Shopify automatically adds a trailing slash to webhook URLs.
1101
+ If your server doesn't handle both /webhooks and /webhooks/,
1102
+ the webhook will 404.
1103
+
1104
+ Common with frameworks that are strict about trailing slashes.
1105
+
1106
+ Recommended fix:
1107
+
1108
+ ## Handle both URL formats
1109
+
1110
+ ```typescript
1111
+ // Remix/React Router - both work by default
1112
+ // app/routes/webhooks.tsx handles /webhooks
1113
+
1114
+ // Express - add middleware
1115
+ app.use((req, res, next) => {
1116
+ if (req.path.endsWith('/') && req.path.length > 1) {
1117
+ const query = req.url.slice(req.path.length);
1118
+ const safePath = req.path.slice(0, -1);
1119
+ res.redirect(301, safePath + query);
1120
+ }
1121
+ next();
1122
+ });
1123
+ ```
1124
+
1125
+ ## Configure web server
1126
+
1127
+ ```nginx
1128
+ # Nginx - strip trailing slashes
1129
+ location ~ ^(.+)/$ {
1130
+ return 301 $1;
1131
+ }
1132
+
1133
+ # Or rewrite to handler
1134
+ location /webhooks {
1135
+ try_files $uri $uri/ @webhooks;
1136
+ }
1137
+ location @webhooks {
1138
+ proxy_pass http://app:3000/webhooks;
1139
+ }
1140
+ ```
1141
+
1142
+ ## Test both formats
1143
+
1144
+ ```bash
1145
+ # Test without slash
1146
+ curl -X POST https://your-app.com/webhooks
1147
+
1148
+ # Test with slash
1149
+ curl -X POST https://your-app.com/webhooks/
1150
+ ```
1151
+
1152
+ ### REST API Required Migration to GraphQL (April 2025)
1153
+
1154
+ Severity: HIGH
1155
+
1156
+ Situation: Building new public apps or maintaining existing
1157
+
1158
+ Symptoms:
1159
+ App store submission rejected for REST API usage.
1160
+ Deprecation warnings in console.
1161
+ Some REST endpoints stop working.
1162
+ Missing features only in GraphQL.
1163
+
1164
+ Why this breaks:
1165
+ As of October 2024, REST Admin API is legacy.
1166
+ Starting April 2025, new public apps MUST use GraphQL.
1167
+
1168
+ REST endpoints will continue working for existing apps,
1169
+ but new features are GraphQL-only.
1170
+
1171
+ Metafields, bulk operations, and many new features
1172
+ require GraphQL.
1173
+
1174
+ Recommended fix:
1175
+
1176
+ ## Use GraphQL for all new code
1177
+
1178
+ ```typescript
1179
+ // REST (legacy)
1180
+ const response = await fetch(
1181
+ `https://${shop}/admin/api/2024-10/products.json`,
1182
+ {
1183
+ headers: { "X-Shopify-Access-Token": token },
1184
+ }
1185
+ );
1186
+
1187
+ // GraphQL (recommended)
1188
+ const response = await admin.graphql(`
1189
+ query {
1190
+ products(first: 10) {
1191
+ edges {
1192
+ node {
1193
+ id
1194
+ title
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+ `);
1200
+ ```
1201
+
1202
+ ## Migrate existing REST calls
1203
+
1204
+ ```typescript
1205
+ // REST: GET /products/{id}.json
1206
+ // GraphQL equivalent:
1207
+ const response = await admin.graphql(`
1208
+ query GetProduct($id: ID!) {
1209
+ product(id: $id) {
1210
+ id
1211
+ title
1212
+ status
1213
+ variants(first: 10) {
1214
+ edges {
1215
+ node {
1216
+ id
1217
+ price
1218
+ inventoryQuantity
1219
+ }
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ `, {
1225
+ variables: { id: `gid://shopify/Product/${productId}` },
1226
+ });
1227
+ ```
1228
+
1229
+ ## Use GraphQL for webhooks too
1230
+
1231
+ ```toml
1232
+ # shopify.app.toml
1233
+ [webhooks]
1234
+ api_version = "2024-10" # Use latest GraphQL version
1235
+ ```
1236
+
1237
+ ### App Bridge Required for Built for Shopify (July 2025)
1238
+
1239
+ Severity: HIGH
1240
+
1241
+ Situation: Building embedded Shopify apps
1242
+
1243
+ Symptoms:
1244
+ App rejected from "Built for Shopify" program.
1245
+ App not appearing correctly in admin.
1246
+ Navigation and chrome issues.
1247
+ Warning about App Bridge version.
1248
+
1249
+ Why this breaks:
1250
+ Effective July 2025, all apps seeking "Built for Shopify" status
1251
+ must use the latest version of App Bridge and be embedded.
1252
+
1253
+ Apps using old App Bridge versions or not embedded will
1254
+ lose built for Shopify benefits (better placement, badges).
1255
+
1256
+ Shopify now serves App Bridge and Polaris via unversioned
1257
+ script tags that auto-update.
1258
+
1259
+ Recommended fix:
1260
+
1261
+ ## Use latest App Bridge via script tag
1262
+
1263
+ ```html
1264
+ <!-- Automatically stays up to date -->
1265
+ <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
1266
+ ```
1267
+
1268
+ ## Use AppProvider in React
1269
+
1270
+ ```typescript
1271
+ // app/routes/app.tsx
1272
+ import { AppProvider } from "@shopify/shopify-app-remix/react";
1273
+
1274
+ export default function App() {
1275
+ return (
1276
+ <AppProvider isEmbeddedApp apiKey={apiKey}>
1277
+ <Outlet />
1278
+ </AppProvider>
1279
+ );
1280
+ }
1281
+ ```
1282
+
1283
+ ## Enable embedded auth strategy
1284
+
1285
+ ```typescript
1286
+ // shopify.server.ts
1287
+ const shopify = shopifyApp({
1288
+ // ...
1289
+ future: {
1290
+ unstable_newEmbeddedAuthStrategy: true,
1291
+ },
1292
+ });
1293
+ ```
1294
+
1295
+ ## Check embedded status
1296
+
1297
+ ```typescript
1298
+ import { useAppBridge } from "@shopify/app-bridge-react";
1299
+
1300
+ function MyComponent() {
1301
+ const app = useAppBridge();
1302
+ const isEmbedded = app.hostOrigin !== window.location.origin;
1303
+ }
1304
+ ```
1305
+
1306
+ ### Missing GDPR Webhooks Block App Store Approval
1307
+
1308
+ Severity: HIGH
1309
+
1310
+ Situation: Submitting app to Shopify App Store
1311
+
1312
+ Symptoms:
1313
+ App submission rejected.
1314
+ "GDPR webhooks not implemented" error.
1315
+ Manual review fails for compliance.
1316
+ Data request webhooks not handled.
1317
+
1318
+ Why this breaks:
1319
+ Shopify requires all apps to handle three GDPR webhooks:
1320
+ 1. customers/data_request - Provide customer data
1321
+ 2. customers/redact - Delete customer data
1322
+ 3. shop/redact - Delete all shop data
1323
+
1324
+ These are automatically subscribed when you create an app.
1325
+ You MUST implement handlers even if you don't store data.
1326
+
1327
+ Recommended fix:
1328
+
1329
+ ## Implement all GDPR handlers
1330
+
1331
+ ```typescript
1332
+ // app/routes/webhooks.tsx
1333
+ export const action = async ({ request }: ActionFunctionArgs) => {
1334
+ const { topic, payload, shop } = await authenticate.webhook(request);
1335
+
1336
+ switch (topic) {
1337
+ case "CUSTOMERS_DATA_REQUEST":
1338
+ await handleDataRequest(shop, payload);
1339
+ break;
1340
+
1341
+ case "CUSTOMERS_REDACT":
1342
+ await handleCustomerRedact(shop, payload);
1343
+ break;
1344
+
1345
+ case "SHOP_REDACT":
1346
+ await handleShopRedact(shop, payload);
1347
+ break;
1348
+ }
1349
+
1350
+ return new Response(null, { status: 200 });
1351
+ };
1352
+
1353
+ async function handleDataRequest(shop: string, payload: any) {
1354
+ const customerId = payload.customer.id;
1355
+
1356
+ // Return customer data within 30 days
1357
+ // Usually send to data_request.destination_url
1358
+ const customerData = await db.customer.findUnique({
1359
+ where: { shopifyId: customerId, shop },
1360
+ });
1361
+
1362
+ if (customerData) {
1363
+ // Send to provided URL or email
1364
+ await sendDataToMerchant(payload.data_request, customerData);
1365
+ }
1366
+ }
1367
+
1368
+ async function handleCustomerRedact(shop: string, payload: any) {
1369
+ const customerId = payload.customer.id;
1370
+
1371
+ // Delete customer's personal data
1372
+ await db.customer.deleteMany({
1373
+ where: { shopifyId: customerId, shop },
1374
+ });
1375
+
1376
+ await db.order.updateMany({
1377
+ where: { customerId, shop },
1378
+ data: { customerEmail: null, customerName: null },
1379
+ });
1380
+ }
1381
+
1382
+ async function handleShopRedact(shop: string, payload: any) {
1383
+ // Shop uninstalled 48+ hours ago
1384
+ // Delete ALL data for this shop
1385
+ await db.session.deleteMany({ where: { shop } });
1386
+ await db.customer.deleteMany({ where: { shop } });
1387
+ await db.order.deleteMany({ where: { shop } });
1388
+ await db.settings.deleteMany({ where: { shop } });
1389
+ }
1390
+ ```
1391
+
1392
+ ## Even if you store nothing
1393
+
1394
+ ```typescript
1395
+ // You must still respond 200
1396
+ case "CUSTOMERS_DATA_REQUEST":
1397
+ case "CUSTOMERS_REDACT":
1398
+ case "SHOP_REDACT":
1399
+ // No data stored, but must acknowledge
1400
+ console.log(`GDPR ${topic} for ${shop} - no data stored`);
1401
+ break;
1402
+ ```
1403
+
1404
+ ## Validation Checks
1405
+
1406
+ ### Hardcoded Shopify API Secret
1407
+
1408
+ Severity: ERROR
1409
+
1410
+ API secrets must never be hardcoded
1411
+
1412
+ Message: Hardcoded Shopify API secret. Use environment variables.
1413
+
1414
+ ### Hardcoded Shopify API Key
1415
+
1416
+ Severity: ERROR
1417
+
1418
+ API keys should use environment variables
1419
+
1420
+ Message: Hardcoded Shopify API key. Use environment variables.
1421
+
1422
+ ### Missing HMAC Verification
1423
+
1424
+ Severity: ERROR
1425
+
1426
+ Webhook endpoints must verify HMAC signature
1427
+
1428
+ Message: Webhook handler without HMAC verification. Use authenticate.webhook().
1429
+
1430
+ ### Synchronous Webhook Processing
1431
+
1432
+ Severity: WARNING
1433
+
1434
+ Webhook handlers should respond quickly
1435
+
1436
+ Message: Multiple await calls in webhook handler. Consider async processing.
1437
+
1438
+ ### Missing Webhook Response
1439
+
1440
+ Severity: ERROR
1441
+
1442
+ Webhooks must return 200 status
1443
+
1444
+ Message: Webhook handler may not return proper response.
1445
+
1446
+ ### Duplicate Webhook Registration
1447
+
1448
+ Severity: WARNING
1449
+
1450
+ Webhooks should be defined in TOML only
1451
+
1452
+ Message: Code-based webhook registration. Define webhooks in shopify.app.toml.
1453
+
1454
+ ### REST API Usage
1455
+
1456
+ Severity: INFO
1457
+
1458
+ REST API is deprecated, use GraphQL
1459
+
1460
+ Message: REST API usage detected. Consider migrating to GraphQL.
1461
+
1462
+ ### Missing Rate Limit Handling
1463
+
1464
+ Severity: WARNING
1465
+
1466
+ API calls should handle 429 responses
1467
+
1468
+ Message: API call without rate limit handling. Implement retry logic.
1469
+
1470
+ ### In-Memory Session Storage
1471
+
1472
+ Severity: WARNING
1473
+
1474
+ In-memory sessions don't scale
1475
+
1476
+ Message: In-memory session storage. Use PrismaSessionStorage or similar.
1477
+
1478
+ ### Missing Session Validation
1479
+
1480
+ Severity: ERROR
1481
+
1482
+ Routes should validate session
1483
+
1484
+ Message: Loader without authentication. Use authenticate.admin(request).
1485
+
1486
+ ## Collaboration
1487
+
1488
+ ### Delegation Triggers
1489
+
1490
+ - user needs payment processing -> stripe-integration (Shopify Payments or Stripe integration)
1491
+ - user needs custom authentication -> auth-specialist (Beyond Shopify OAuth)
1492
+ - user needs email/SMS notifications -> twilio-communications (Customer notifications outside Shopify)
1493
+ - user needs AI features -> llm-architect (Product descriptions, chatbots)
1494
+ - user needs serverless deployment -> aws-serverless (Lambda or Vercel deployment)
45
1495
 
46
1496
  ## When to Use
47
- This skill is applicable to execute the workflow or actions described in the overview.
1497
+
1498
+ - User mentions or implies: shopify app
1499
+ - User mentions or implies: shopify
1500
+ - User mentions or implies: embedded app
1501
+ - User mentions or implies: polaris
1502
+ - User mentions or implies: app bridge
1503
+ - User mentions or implies: shopify webhook