← Shopify App BuilderCONTENT HISTORY

Update to Shopify App Builder

Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "When asked to build Shopify admin apps, use App Bridge 4.x web components, shopify global API, session tokens, JWT validation, and migrations from 3.x. Covers CDN setup, all 8 web components, resource picker API, React hooks, backend JWT validation, and 5 worked examples.",
  "included_files": [],
  "name": "app-bridge",
  "skill_md_contents": "---\nname: app-bridge\ndescription: \"When asked to build Shopify admin apps, use App Bridge 4.x web components, shopify global API, session tokens, JWT validation, and migrations from 3.x. Covers CDN setup, all 8 web components, resource picker API, React hooks, backend JWT validation, and 5 worked examples.\"\n---\n\n# Shopify App Bridge 4.x: Web Components, Sessions & Admin Apps\n\n## When Asked...\n\n- **\"Build a Shopify admin app\"** → Use App Bridge 4.x with web components; load via CDN with data-api-key; use shopify global object for actions\n- **\"Add save/cancel buttons\"** → Use `<ui-save-bar>` web component with data-primary-action and data-secondary-action attributes\n- **\"Show a confirmation modal\"** → Use `<ui-modal>` web component with data-open attribute and slot-based content\n- **\"Display success message\"** → Use `shopify.toast()` method with title, message, duration, isError flags\n- **\"Let user pick products/customers\"** → Use resource picker API: `shopify.resourcePicker({ type: 'product' })`\n- **\"Validate backend requests\"** → Exchange session token for JWT; verify JWT signature with Shopify's public key\n- **\"Upgrade from App Bridge 3.x\"** → Follow migration checklist: remove AppProvider, update component usage, use web components directly\n\n## App Bridge 4.x Architecture\n\nApp Bridge 4.x is a **web components-first framework**. The major shift from 3.x:\n- **No AppProvider needed** — directly use web components and shopify global object\n- **Native web components** — built-in elements like `<ui-modal>`, `<ui-save-bar>`, `<ui-toast>` instead of React/Vue wrappers\n- **shopify global object** — replaces AppBridge context; provides toast(), modal(), navigate(), loading(), idToken(), etc.\n- **Session tokens** — automatic JWT exchange for backend authentication\n- **CDN-first delivery** — loaded via script tag with data-api-key; works in any HTML/framework\n\n## Installation & CDN Setup\n\n### CDN Script (Recommended for Admin Apps)\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Shopify Admin App</title>\n</head>\n<body>\n  <div id=\"app\"></div>\n\n  <script src=\"https://cdn.shopify.com/shopifycloud/app-bridge.js\"\n          data-api-key=\"YOUR_PUBLIC_API_KEY\"\n          data-host=\"{{ request.host }}\">\n  </script>\n\n  <script>\n    // shopify global object is now available\n    console.log(shopify);\n  </script>\n</body>\n</html>\n```\n\n**data-api-key**: Your Shopify public app API key (from shopify.app configuration)\n**data-host**: Base64-encoded host parameter (usually `{{ request.host }}` in server templates)\n\n### npm Package (for React/Node.js apps)\n\n```bash\nnpm install @shopify/app-bridge @shopify/app-bridge-react\n```\n\n```typescript\nimport { initializeApp } from '@shopify/app-bridge';\n\nconst app = initializeApp({\n  apiKey: process.env.REACT_APP_SHOPIFY_API_KEY,\n  host: new URLSearchParams(location.search).get('host'),\n});\n\nconsole.log(window.shopify);\n```\n\n## shopify Global Object API\n\nThe **shopify global** provides the primary API for app interactions:\n\n```typescript\n// Toast notifications\nshopify.toast({\n  title: 'Success',\n  message: 'Settings saved',\n  duration: 3000,\n  isError: false,\n});\n\n// Modal dialogs\nshopify.modal.show({\n  title: 'Confirm Action',\n  message: 'Are you sure?',\n  buttons: [\n    { label: 'Cancel', type: 'secondary' },\n    { label: 'Delete', type: 'primary', isDestructive: true },\n  ],\n});\n\n// Navigation\nshopify.navigate({ name: 'Admin::Product::Index' });\nshopify.navigate({ url: '/admin/products/new' });\n\n// Loading state\nshopify.loading.dispatch(true);\nshopify.loading.dispatch(false);\n\n// Session token (JWT for backend calls)\nconst idToken = await shopify.idToken();\n\n// Current app environment\nshopify.environment // 'Admin' | 'Checkout' | 'Mobile' | 'POS'\nshopify.config // { apiKey, host, theme }\n\n// Deep linking\nshopify.actions.Admin?.navigate({ path: '/products' });\n```\n\n## Web Components API\n\nApp Bridge 4.x provides 8 native web components. Use them directly in HTML without React wrappers.\n\n### 1. `<ui-title-bar>` — Page Header\n\n```html\n<ui-title-bar>\n  <h1 slot=\"title\">Bulk Product Editor</h1>\n  <button slot=\"secondary-actions\">Help</button>\n  <button slot=\"primary-action\" onclick=\"saveProducts()\">Save</button>\n</ui-title-bar>\n```\n\n**Attributes**:\n- `title` (slot) — Page title\n- `primary-action` (slot) — Right-aligned primary button\n- `secondary-actions` (slot) — Right-aligned secondary buttons\n\n### 2. `<ui-save-bar>` — Sticky Save/Discard\n\n```html\n<ui-save-bar\n  data-primary-action=\"Save\"\n  data-secondary-action=\"Discard\"\n  data-save-action-loading=\"false\"\n  onprimaryaction=\"handleSave(event)\"\n  onsecondaryaction=\"handleDiscard(event)\">\n</ui-save-bar>\n\n<script>\n  async function handleSave(event) {\n    event.preventDefault();\n    const token = await shopify.idToken();\n    const formData = new FormData(document.querySelector('form'));\n\n    const res = await fetch('/api/settings', {\n      method: 'POST',\n      body: JSON.stringify(Object.fromEntries(formData)),\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${token}`,\n      },\n    });\n\n    if (res.ok) {\n      shopify.toast({ title: 'Saved' });\n      document.getElementById('form').classList.remove('dirty');\n    }\n  }\n</script>\n```\n\n**Attributes**:\n- `data-primary-action` — Button label (default: \"Save\")\n- `data-secondary-action` — Discard label (default: \"Discard\")\n- `data-save-action-loading` — Show spinner on primary button\n- `onprimaryaction` — Fired when save clicked\n- `onsecondaryaction` — Fired when discard clicked\n\n### 3. `<ui-modal>` — Dialog Box\n\n```html\n<ui-modal data-open=\"true\" data-title=\"Delete Product\">\n  <p>This action cannot be undone.</p>\n  <button slot=\"primary-action\" onclick=\"confirmDelete()\">Delete</button>\n  <button slot=\"secondary-action\" onclick=\"closeModal()\">Cancel</button>\n</ui-modal>\n\n<script>\n  async function confirmDelete() {\n    const token = await shopify.idToken();\n    const res = await fetch('/api/products/123', {\n      method: 'DELETE',\n      headers: { 'Authorization': `Bearer ${token}` },\n    });\n\n    if (res.ok) {\n      shopify.toast({ title: 'Deleted' });\n      document.querySelector('ui-modal').setAttribute('data-open', 'false');\n    }\n  }\n</script>\n```\n\n**Attributes**:\n- `data-open` — Show/hide (\"true\" or \"false\")\n- `data-title` — Modal title\n- `primary-action` (slot) — Primary button\n- `secondary-action` (slot) — Secondary button\n\n### 4. `<ui-toast>` — Toast Notification (Alternative)\n\n```html\n<ui-toast\n  data-message=\"Settings updated\"\n  data-duration=\"3000\"\n  data-is-error=\"false\"\n  data-open=\"true\">\n</ui-toast>\n\n<script>\n  function showSuccess() {\n    const toast = document.querySelector('ui-toast');\n    toast.setAttribute('data-message', 'Successfully saved');\n    toast.setAttribute('data-open', 'true');\n    setTimeout(() => toast.setAttribute('data-open', 'false'), 3000);\n  }\n</script>\n```\n\n### 5. `<ui-nav-menu>` — Sidebar Navigation\n\n```html\n<ui-nav-menu>\n  <a href=\"/dashboard\" slot=\"item\">Dashboard</a>\n  <a href=\"/products\" slot=\"item\" data-active=\"true\">Products</a>\n  <a href=\"/orders\" slot=\"item\">Orders</a>\n  <a href=\"/settings\" slot=\"item\">Settings</a>\n</ui-nav-menu>\n```\n\n### 6. `<ui-resource-picker>` — Product/Collection Picker UI\n\n```html\n<ui-resource-picker\n  data-type=\"product\"\n  data-selectable=\"multiple\"\n  data-can-query-for-more=\"true\"\n  onchange=\"handleResourceSelect(event)\">\n</ui-resource-picker>\n```\n\n### 7. `<ui-print-action>` — Print Button\n\n```html\n<ui-print-action onclick=\"window.print()\">\n  Print Invoice\n</ui-print-action>\n```\n\n### 8. `<s-page>` — Full-Page Container\n\n```html\n<s-page>\n  <ui-title-bar>\n    <h1 slot=\"title\">Dashboard</h1>\n  </ui-title-bar>\n\n  <div style=\"padding: 20px;\">\n    <h2>Welcome</h2>\n  </div>\n</s-page>\n```\n\n## Resource Picker API (Programmatic)\n\nFor programmatic access without UI component:\n\n```typescript\n// Product picker\nawait shopify.resourcePicker({\n  type: 'product',\n  selectionIds: [{ gid: 'gid://shopify/Product/123' }],\n  onSelection(resources) {\n    console.log('Selected:', resources.selection);\n  },\n  onCancel() {\n    console.log('Picker cancelled');\n  },\n});\n\n// Collection picker\nawait shopify.resourcePicker({\n  type: 'collection',\n  onSelection(resources) {\n    // Handle selection\n  },\n});\n\n// Customer picker\nawait shopify.resourcePicker({\n  type: 'customer',\n  onSelection(resources) {\n    const customer = resources.selection[0];\n    console.log(customer.id, customer.email);\n  },\n});\n\n// Variant picker\nawait shopify.resourcePicker({\n  type: 'variant',\n  onSelection(resources) {\n    const variant = resources.selection[0];\n    console.log(variant.id, variant.title, variant.price);\n  },\n});\n```\n\n**Supported types**: `product`, `collection`, `customer`, `variant`, `draft_order`\n\n## App Bridge React Hooks (Optional)\n\nFor React apps, optional hooks simplify shopify global access:\n\n```typescript\nimport { useAppBridge } from '@shopify/app-bridge-react';\nimport { Toast } from '@shopify/app-bridge/actions';\n\nexport function MyComponent() {\n  const app = useAppBridge();\n\n  const handleSave = async () => {\n    app.dispatch({ type: 'LOADING_DISPATCH', payload: true });\n    const token = await app.getSessionToken();\n\n    const res = await fetch('/api/settings', {\n      method: 'POST',\n      headers: { 'Authorization': `Bearer ${token}` },\n    });\n\n    app.dispatch({ type: 'LOADING_DISPATCH', payload: false });\n\n    app.dispatch(Toast.create({\n      title: 'Saved',\n      message: 'Settings updated',\n      duration: 3000,\n    }));\n  };\n\n  return <button onClick={handleSave}>Save</button>;\n}\n```\n\n## Session Token & JWT Backend Validation\n\nApp Bridge automatically provides session tokens (JWTs) for authenticated backend calls.\n\n### Client Side: Get Token & Send\n\n```javascript\nconst idToken = await shopify.idToken();\n\nconst response = await fetch('/api/admin/settings', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'Authorization': `Bearer ${idToken}`,\n  },\n  body: JSON.stringify({ theme_color: '#FF0000' }),\n});\n\nif (!response.ok) {\n  shopify.toast({\n    title: 'Error',\n    message: 'Failed to save settings',\n    isError: true,\n  });\n}\n```\n\n### Backend Side: Validate JWT\n\n**Node.js/Express**:\n\n```typescript\nimport { jwtDecode } from 'jwt-decode';\n\nasync function validateSessionToken(req, res, next) {\n  const authHeader = req.headers.authorization;\n  if (!authHeader || !authHeader.startsWith('Bearer ')) {\n    return res.status(401).json({ error: 'Missing token' });\n  }\n\n  const token = authHeader.slice(7);\n\n  try {\n    const decoded = jwtDecode(token);\n\n    if (!decoded.iss || !decoded.iss.includes('shopify.com')) {\n      throw new Error('Invalid issuer');\n    }\n\n    if (!decoded.aud || decoded.aud !== process.env.SHOPIFY_API_KEY) {\n      throw new Error('Invalid audience');\n    }\n\n    if (decoded.exp && Date.now() >= decoded.exp * 1000) {\n      throw new Error('Token expired');\n    }\n\n    req.shop = decoded.dest;\n    req.userId = decoded.sub;\n    next();\n  } catch (err) {\n    return res.status(401).json({ error: 'Invalid token' });\n  }\n}\n\napp.post('/api/admin/settings', validateSessionToken, (req, res) => {\n  console.log(`User ${req.userId} from shop ${req.shop} updating settings`);\n  res.json({ success: true });\n});\n```\n\n**Python/Flask**:\n\n```python\nfrom flask import request, jsonify\nfrom jwt import decode as jwt_decode\nfrom functools import wraps\n\ndef validate_session_token(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        auth_header = request.headers.get('Authorization', '')\n        if not auth_header.startswith('Bearer '):\n            return jsonify({'error': 'Missing token'}), 401\n\n        token = auth_header[7:]\n\n        try:\n            decoded = jwt_decode(token, options={\"verify_signature\": False})\n\n            if not decoded.get('iss') or 'shopify.com' not in decoded['iss']:\n                raise ValueError('Invalid issuer')\n\n            if decoded.get('aud') != os.getenv('SHOPIFY_API_KEY'):\n                raise ValueError('Invalid audience')\n\n            request.shop = decoded.get('dest')\n            request.user_id = decoded.get('sub')\n            return f(*args, **kwargs)\n\n        except Exception as e:\n            return jsonify({'error': 'Invalid token'}), 401\n\n    return decorated_function\n\n@app.post('/api/admin/settings')\n@validate_session_token\ndef update_settings():\n    shop = request.shop\n    user_id = request.user_id\n    data = request.get_json()\n    return jsonify({'success': True})\n```\n\n## Worked Examples\n\n### Example 1: Save Bar with Form Validation\n\n```html\n<!DOCTYPE html>\n<html>\n<body>\n  <script src=\"https://cdn.shopify.com/shopifycloud/app-bridge.js\"\n          data-api-key=\"pk_test_12345\"\n          data-host=\"example.myshopify.com\">\n  </script>\n\n  <ui-title-bar>\n    <h1 slot=\"title\">Product Settings</h1>\n  </ui-title-bar>\n\n  <form id=\"settings-form\" style=\"padding: 20px; max-width: 600px;\">\n    <label>\n      Product Name\n      <input type=\"text\" name=\"product_name\" required>\n    </label>\n    <br><br>\n\n    <label>\n      Price\n      <input type=\"number\" name=\"price\" step=\"0.01\" required>\n    </label>\n    <br><br>\n\n    <label>\n      Description\n      <textarea name=\"description\"></textarea>\n    </label>\n  </form>\n\n  <ui-save-bar\n    data-primary-action=\"Save Changes\"\n    data-secondary-action=\"Discard\"\n    onprimaryaction=\"handleSave(event)\"\n    onsecondaryaction=\"handleDiscard(event)\">\n  </ui-save-bar>\n\n  <script>\n    const form = document.getElementById('settings-form');\n\n    form.addEventListener('change', () => {\n      form.classList.add('dirty');\n    });\n\n    async function handleSave(event) {\n      event.preventDefault();\n\n      if (!form.checkValidity()) {\n        shopify.toast({\n          title: 'Validation Error',\n          message: 'Please fill all required fields',\n          isError: true,\n        });\n        return;\n      }\n\n      const token = await shopify.idToken();\n      const formData = new FormData(form);\n\n      try {\n        const response = await fetch('/api/products/settings', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n            'Authorization': `Bearer ${token}`,\n          },\n          body: JSON.stringify(Object.fromEntries(formData)),\n        });\n\n        if (!response.ok) throw new Error('Save failed');\n\n        shopify.toast({\n          title: 'Success',\n          message: 'Product settings saved',\n        });\n        form.classList.remove('dirty');\n      } catch (err) {\n        shopify.toast({\n          title: 'Error',\n          message: err.message,\n          isError: true,\n        });\n      }\n    }\n\n    function handleDiscard(event) {\n      event.preventDefault();\n      form.reset();\n      form.classList.remove('dirty');\n    }\n  </script>\n</body>\n</html>\n```\n\n### Example 2: Destructive Modal (Delete Confirmation)\n\n```typescript\nasync function showDeleteConfirmation(productId: string) {\n  const modal = document.createElement('ui-modal');\n  modal.setAttribute('data-open', 'true');\n  modal.setAttribute('data-title', 'Delete Product');\n\n  const content = document.createElement('p');\n  content.textContent = 'This action cannot be undone.';\n\n  const confirmBtn = document.createElement('button');\n  confirmBtn.setAttribute('slot', 'primary-action');\n  confirmBtn.textContent = 'Delete';\n\n  const cancelBtn = document.createElement('button');\n  cancelBtn.setAttribute('slot', 'secondary-action');\n  cancelBtn.textContent = 'Cancel';\n\n  modal.appendChild(content);\n  modal.appendChild(confirmBtn);\n  modal.appendChild(cancelBtn);\n  document.body.appendChild(modal);\n\n  confirmBtn.onclick = async () => {\n    shopify.loading.dispatch(true);\n    const token = await shopify.idToken();\n\n    try {\n      const res = await fetch(`/api/products/${productId}`, {\n        method: 'DELETE',\n        headers: { 'Authorization': `Bearer ${token}` },\n      });\n\n      shopify.loading.dispatch(false);\n      modal.setAttribute('data-open', 'false');\n\n      shopify.toast({\n        title: 'Product Deleted',\n        message: 'The product has been permanently removed',\n      });\n\n      shopify.navigate({ name: 'Admin::Product::Index' });\n    } catch (err) {\n      shopify.loading.dispatch(false);\n      shopify.toast({\n        title: 'Error',\n        message: `Failed to delete: ${err.message}`,\n        isError: true,\n      });\n    }\n  };\n\n  cancelBtn.onclick = () => {\n    modal.setAttribute('data-open', 'false');\n    modal.remove();\n  };\n}\n```\n\n### Example 3: Product Resource Picker\n\n```typescript\nasync function openProductSelector() {\n  try {\n    await shopify.resourcePicker({\n      type: 'product',\n      selectionIds: [],\n      onSelection(resources) {\n        const products = resources.selection;\n\n        products.forEach(product => {\n          const item = document.createElement('div');\n          item.style.cssText = 'padding: 10px; border: 1px solid #ddd; margin: 5px 0;';\n          item.innerHTML = `\n            <strong>${product.title}</strong><br>\n            ID: ${product.id}<br>\n            <img src=\"${product.image?.originalSrc}\" style=\"width: 50px;\">\n          `;\n          document.getElementById('product-list').appendChild(item);\n        });\n\n        shopify.toast({\n          title: 'Success',\n          message: `${products.length} products selected`,\n        });\n      },\n      onCancel() {\n        shopify.toast({\n          title: 'Cancelled',\n          message: 'Product selection cancelled',\n        });\n      },\n    });\n  } catch (err) {\n    shopify.toast({\n      title: 'Error',\n      message: `Failed to open picker: ${err.message}`,\n      isError: true,\n    });\n  }\n}\n```\n\n### Example 4: Toast on Success with Error Handling\n\n```typescript\nasync function bulkUpdateProducts(productIds: string[]) {\n  shopify.loading.dispatch(true);\n  const token = await shopify.idToken();\n\n  const results = { success: 0, failed: 0 };\n\n  for (const productId of productIds) {\n    try {\n      const res = await fetch(`/api/products/${productId}/sync`, {\n        method: 'POST',\n        headers: {\n          'Authorization': `Bearer ${token}`,\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ syncInventory: true }),\n      });\n\n      if (res.ok) results.success++;\n      else results.failed++;\n    } catch (err) {\n      results.failed++;\n    }\n  }\n\n  shopify.loading.dispatch(false);\n\n  const message = results.failed > 0\n    ? `${results.success} succeeded, ${results.failed} failed`\n    : `All ${results.success} products synced`;\n\n  shopify.toast({\n    title: 'Bulk Update Complete',\n    message,\n    isError: results.failed > 0,\n    duration: 5000,\n  });\n}\n```\n\n### Example 5: Navigation Menu & Routing\n\n```typescript\nfunction setupNavigation() {\n  const navMenu = document.querySelector('ui-nav-menu');\n  const routes = [\n    { path: '/dashboard', label: 'Dashboard', icon: 'home' },\n    { path: '/products', label: 'Products', icon: 'package' },\n    { path: '/orders', label: 'Orders', icon: 'bag' },\n    { path: '/settings', label: 'Settings', icon: 'gear' },\n  ];\n\n  routes.forEach(route => {\n    const link = document.createElement('a');\n    link.href = route.path;\n    link.setAttribute('slot', 'item');\n    link.textContent = route.label;\n\n    if (window.location.pathname === route.path) {\n      link.setAttribute('data-active', 'true');\n    }\n\n    link.addEventListener('click', (e) => {\n      e.preventDefault();\n      navMenu.querySelectorAll('a').forEach(a => a.removeAttribute('data-active'));\n      link.setAttribute('data-active', 'true');\n      shopify.navigate({ url: route.path });\n    });\n\n    navMenu.appendChild(link);\n  });\n}\n\nsetupNavigation();\n```\n\n## Migration Checklist: App Bridge 3.x → 4.x\n\n1. **Remove AppProvider** — No longer needed; shopify global is auto-initialized\n2. **Update web component imports** — Use native `<ui-*>` elements instead of React wrappers\n3. **Replace useAppBridge hook** — Use `window.shopify` or optional `useAppBridge()` hook from React package\n4. **Update toast/modal calls** — `shopify.toast()` and `shopify.modal.show()` instead of Toast/Modal actions\n5. **Session tokens automatic** — No need to manually request; `shopify.idToken()` handles refresh\n6. **Update resource picker** — Use `shopify.resourcePicker()` API or `<ui-resource-picker>` component\n7. **Test JWT validation** — Ensure backend correctly decodes and validates JWTs\n\n## Frame Ancestors & CSP Setup\n\nConfigure Content Security Policy to allow App Bridge:\n\n```html\n<meta http-equiv=\"Content-Security-Policy\"\n      content=\"frame-ancestors https://admin.shopify.com https://*.myshopify.com;\">\n```\n\nOr in server headers (Express.js example):\n\n```typescript\napp.use((req, res, next) => {\n  res.set('Frame-Ancestors', 'https://admin.shopify.com https://*.myshopify.com');\n  next();\n});\n```\n\nThis allows your app to be embedded in Shopify Admin iframe.\n"
}

SHA-256 of public snapshot: 9024c96ce4a3fe47f0bbc26edd1e1f19dd185fd6055eb01ce62876b6aa233b12