← Shopify App BuilderCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Shopify App Builder
Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull 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