
Shopify App Development
- 58 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build embedded Shopify apps using Remix, App Bridge, Polaris components, and the OAuth authentication flow.
About
Builds embedded Shopify apps with the Remix framework, App Bridge, Polaris UI, and OAuth. A developer uses it to create installable apps for the Shopify App Store or merchants.
- Remix framework with App Bridge and Polaris UI
- OAuth authentication flow for embedded apps
Shopify App Development by the numbers
- 58 all-time installs (skills.sh)
- Ranked #3,178 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-app-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build embedded Shopify apps using Remix, App Bridge, Polaris components, and the OAuth authentication flow.
Files
Shopify App Development
Overview
Build Shopify apps using the Shopify CLI 3.x Remix template, which handles OAuth token exchange, session storage, and App Bridge initialization automatically. Embedded apps run inside the Shopify Admin iframe and use Polaris for a native-feeling UI. The modern approach uses the Remix-based @shopify/shopify-app-remix package rather than the legacy Express template.
When to Use This Skill
- When building a public or custom Shopify app that extends Admin functionality
- When creating an embedded app that merchants install from the Shopify App Store
- When implementing OAuth for the first time with session persistence across reinstalls
- When needing to access the Admin API on behalf of authenticated merchants
- When building merchant-facing tooling with Shopify's Polaris design system
- When replacing an older Express/koa-based Shopify app with the modern Remix stack
Core Instructions
1. Scaffold the app with Shopify CLI
npm install -g @shopify/cli @shopify/theme
shopify app init my-shopify-app
# Choose: Remix template
cd my-shopify-app
shopify app devThis scaffolds a Remix app with OAuth, session storage (SQLite by default), and App Bridge already wired up. The dev command tunnels your local server via Cloudflare and installs the app on your Partner development store.
2. Understand the OAuth flow and session handling
The scaffold uses @shopify/shopify-app-remix which handles the OAuth dance. In app/shopify.server.ts:
import "@shopify/shopify-app-remix/adapters/node";
import {
AppDistribution,
DeliveryMethod,
shopifyApp,
LATEST_API_VERSION,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
apiVersion: LATEST_API_VERSION,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL || "",
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session }) => {
shopify.registerWebhooks({ session });
},
},
});
export default shopify;
export const authenticate = shopify.authenticate;3. Protect routes and call the Admin API
Any loader or action that needs Admin API access calls authenticate.admin:
// app/routes/app._index.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin, session } = await authenticate.admin(request);
// GraphQL Admin API call
const response = await admin.graphql(`
query {
shop {
name
email
primaryDomain { url }
}
}
`);
const { data } = await response.json();
return json({ shop: data.shop });
};
export default function Index() {
const { shop } = useLoaderData<typeof loader>();
return <Page title={`Hello, ${shop.name}`} />;
}4. Build UI with Polaris components
// app/routes/app.products.tsx
import {
Page,
Layout,
Card,
DataTable,
Button,
Banner,
} from "@shopify/polaris";
import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
export default function ProductsPage() {
const shopify = useAppBridge();
const handleSave = async () => {
// Use App Bridge Toast for notifications inside the iframe
shopify.toast.show("Products updated successfully");
};
return (
<Page>
<TitleBar title="Products" primaryAction={{ content: "Save", onAction: handleSave }} />
<Layout>
<Layout.Section>
<Card>
<DataTable
columnContentTypes={["text", "numeric", "numeric"]}
headings={["Product", "Price", "Inventory"]}
rows={[["Widget A", "$19.99", 42]]}
/>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}5. Configure scopes and handle app reinstallation
Define required scopes in shopify.app.toml:
name = "my-shopify-app"
client_id = "your_api_key"
application_url = "https://your-app.fly.dev"
embedded = true
[access_scopes]
scopes = "read_products,write_products,read_orders"
[webhooks]
api_version = "2025-01"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks"Handle the GDPR mandatory webhooks (customers/data_request, customers/redact, shop/redact) even if your app does not store personal data — Shopify requires these endpoints.
6. Deploy to production
shopify app deploy
# Deploys to Shopify (functions/extensions)
# Deploy the Remix server separately (Fly.io, Railway, Render)
fly launch
fly deployExamples
Mutation via Admin GraphQL API
// Create a product via the Admin API inside a Remix action
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(
`#graphql
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
}
userErrors {
field
message
}
}
}`,
{
variables: {
input: {
title: "New Product",
vendor: "My Store",
productType: "Widget",
tags: ["new", "featured"],
},
},
}
);
const { data } = await response.json();
if (data.productCreate.userErrors.length > 0) {
return json({ errors: data.productCreate.userErrors }, { status: 422 });
}
return json({ product: data.productCreate.product });
};App Bridge Resource Picker (v4)
import { useAppBridge } from "@shopify/app-bridge-react";
import { useState } from "react";
import { Button } from "@shopify/polaris";
export default function ProductSelector() {
const shopify = useAppBridge();
const [selected, setSelected] = useState<string[]>([]);
const handleSelectProducts = async () => {
const selection = await shopify.resourcePicker({
type: "product",
multiple: true,
});
if (selection) {
setSelected(selection.map((p) => p.id));
}
};
return (
<>
<Button onClick={handleSelectProducts}>Select Products</Button>
<p>Selected IDs: {selected.join(", ")}</p>
</>
);
}Best Practices
- Use the Remix CLI template — it handles session storage, CSRF, OAuth token refresh, and frame-ancestor CSP headers automatically
- Store sessions in a persistent database (Prisma + PostgreSQL in production) — the default SQLite storage is unsuitable for multi-instance deployments
- Scope creep hurts conversion — only request the minimum scopes needed; merchants see the scope list during installation
- Use App Bridge for navigation and modals — direct
window.locationnavigation breaks the embedded iframe context - Validate webhook HMAC signatures — even for mandatory GDPR webhooks that you don't act on
- Test reinstall flows — merchants who uninstall and reinstall must receive fresh OAuth tokens without stale session data
- Use `LATEST_API_VERSION` in development only — pin to a specific version (e.g.,
2025-01) in production to avoid breaking changes - Handle `payment_required` errors — Apps on the App Store may encounter billing requirement errors if merchants exceed their plan
Common Pitfalls
| Problem | Solution |
|---|---|
| "Refused to display in frame" CSP error | Ensure your Remix server returns frame-ancestors https://*.myshopify.com https://admin.shopify.com in Content-Security-Policy |
| OAuth redirect loop after install | Check that your app URL in shopify.app.toml matches the URL your server is reachable at — mismatch causes infinite redirects |
| Session not found on subsequent requests | Use a persistent session storage (Prisma/PostgreSQL); SQLite doesn't work across Fly.io or Render instances |
App Bridge useAppBridge() returns null | Wrap your Remix app root with <AppProvider> from @shopify/shopify-app-remix/react |
| Webhooks registered but not firing | Webhooks registered during afterAuth may not persist after app update — call shopify.registerWebhooks in a separate route for verification |
| "Invalid HMAC" on webhook endpoint | Ensure raw body is read before any JSON parsing middleware — use getRawBody before Express/Remix body parsing |
Related Skills
- @shopify-admin-api
- @shopify-webhooks
- @shopify-checkout-extensions
- @shopify-storefront-api
- @oauth-implementation
{
"context": "Tests whether the agent builds an embedded Shopify app UI page using Polaris components and App Bridge patterns correctly, including the ResourcePicker for product selection, Toast for feedback, TitleBar for page headers, and avoiding iframe-breaking patterns like window.location navigation or browser alert().",
"type": "weighted_checklist",
"checklist": [
{
"name": "Polaris Page component",
"max_score": 7,
"description": "app/routes/app.tagging.tsx uses the <Page> component from @shopify/polaris as the top-level container"
},
{
"name": "Polaris Card or LegacyCard",
"max_score": 6,
"description": "app/routes/app.tagging.tsx uses <Card> (or <LegacyCard>) from @shopify/polaris to group content"
},
{
"name": "TitleBar from App Bridge",
"max_score": 9,
"description": "app/routes/app.tagging.tsx imports and renders <TitleBar> from @shopify/app-bridge-react"
},
{
"name": "useAppBridge hook present",
"max_score": 8,
"description": "app/routes/app.tagging.tsx calls useAppBridge() to get the shopify App Bridge instance"
},
{
"name": "App Bridge Toast for feedback",
"max_score": 12,
"description": "app/routes/app.tagging.tsx calls shopify.toast.show(...) for success or error notification (not browser alert() or window.alert())"
},
{
"name": "No browser alert()",
"max_score": 8,
"description": "app/routes/app.tagging.tsx does NOT call alert(), window.alert(), or confirm() for any user notification"
},
{
"name": "ResourcePicker for product selection",
"max_score": 10,
"description": "app/routes/app.tagging.tsx uses <ResourcePicker> from @shopify/app-bridge-react to allow product selection"
},
{
"name": "No window.location navigation",
"max_score": 9,
"description": "app/routes/app.tagging.tsx does NOT use window.location.href, window.location.assign(), or window.location.replace() for navigation"
},
{
"name": "authenticate.admin in loader/action",
"max_score": 10,
"description": "app/routes/app.tagging.tsx calls authenticate.admin(request) in at least one loader or action function"
},
{
"name": "Admin GraphQL API call",
"max_score": 11,
"description": "app/routes/app.tagging.tsx calls admin.graphql(...) to interact with the Shopify Admin API"
},
{
"name": "Polaris Layout component",
"max_score": 10,
"description": "app/routes/app.tagging.tsx uses <Layout> and/or <Layout.Section> from @shopify/polaris for page structure"
}
]
}
Build a Product Tagging Page for a Shopify Embedded App
Problem/Feature Description
A Shopify agency is building a bulk product tagging tool for merchants. Merchants need to be able to select products from their store, apply a set of tags to them all at once, and get confirmation feedback — all without leaving the Shopify Admin interface. The tool is a Remix-based embedded app running inside the Shopify Admin iframe.
The UI team has been told to make the tagging page feel like a native part of the Shopify Admin — not a third-party app bolted on. That means using Shopify's own component library throughout, following Shopify's interaction patterns for resource selection and notifications, and navigating between pages in a way that doesn't break the embedded iframe environment. A previous prototype tried using browser-native elements and standard React Router navigation, but merchants reported the page "flickering" and browser alert popups that looked jarring and out of place.
Write the Remix route file for this product tagging page. The page should allow a merchant to open a product picker, display selected products, accept a tag input, submit the tag via a Remix action that calls the Admin GraphQL API, and confirm success to the merchant. The GraphQL mutation and the session authentication should be included.
Output Specification
app/routes/app.tagging.tsx— the full Remix route with loader, action, and React component for the product tagging page
The file should be complete and self-contained (imports, loader/action, and component all in one file). You may reference ../shopify.server for the authenticate export.
{
"context": "Tests whether the agent correctly implements mandatory GDPR webhook handlers and HMAC-based webhook authentication for a Shopify App Store submission, including all three required GDPR topics and proper signature verification.",
"type": "weighted_checklist",
"checklist": [
{
"name": "CUSTOMERS_DATA_REQUEST handler",
"max_score": 9,
"description": "webhooks.tsx includes a case or handler for the \"CUSTOMERS_DATA_REQUEST\" topic"
},
{
"name": "CUSTOMERS_REDACT handler",
"max_score": 9,
"description": "webhooks.tsx includes a case or handler for the \"CUSTOMERS_REDACT\" topic"
},
{
"name": "SHOP_REDACT handler",
"max_score": 9,
"description": "webhooks.tsx includes a case or handler for the \"SHOP_REDACT\" topic"
},
{
"name": "APP_UNINSTALLED retained",
"max_score": 8,
"description": "webhooks.tsx still handles the \"APP_UNINSTALLED\" topic (not removed from the original)"
},
{
"name": "HMAC verification present",
"max_score": 12,
"description": "webhooks.tsx performs HMAC signature verification on the incoming webhook request (via authenticate.webhook, manual HMAC check, or equivalent)"
},
{
"name": "Raw body before parsing",
"max_score": 10,
"description": "WEBHOOK_NOTES.md or code comments mention that raw body must be read before JSON parsing to validate HMAC correctly, OR code demonstrates this pattern"
},
{
"name": "Notes: GDPR topics required by Shopify",
"max_score": 10,
"description": "WEBHOOK_NOTES.md states that the three GDPR topics (customers/data_request, customers/redact, shop/redact) are mandatory requirements for App Store submission"
},
{
"name": "Notes: HMAC validation purpose",
"max_score": 9,
"description": "WEBHOOK_NOTES.md explains that HMAC validation confirms the request originated from Shopify"
},
{
"name": "GDPR handlers contain real logic",
"max_score": 12,
"description": "The CUSTOMERS_REDACT and/or SHOP_REDACT handlers contain actual data deletion or redaction logic (not just empty stubs or comments saying 'TODO')"
},
{
"name": "No unauthenticated webhook path",
"max_score": 12,
"description": "webhooks.tsx does NOT process or act on webhook payload before authentication/HMAC verification completes"
}
]
}
Set Up Webhook Handling for Shopify App Store Submission
Problem/Feature Description
A startup has built a Shopify app that monitors inventory levels and sends alerts to merchants. The app is nearly ready to submit to the Shopify App Store, but the legal and compliance team has flagged that the app doesn't yet meet Shopify's data protection requirements. Shopify requires that every App Store app handles specific data-related webhook topics and can prove its webhook endpoints are authentic — otherwise Shopify will reject the app submission.
The backend engineer needs to write the webhook handler route that covers all required topics. The app stores some merchant and customer data (product watchlists associated with customer emails), so the team needs real logic for the data deletion and redaction topics, not just stubs. In addition, the security architect has pointed out that the current webhook endpoint blindly trusts all incoming requests without verifying they genuinely came from Shopify — this needs to be fixed. Produce a working webhook handler and a WEBHOOK_NOTES.md documenting what was implemented and why each piece is required.
Output Specification
Produce the following files:
app/routes/webhooks.tsx(orapp/routes/webhooks.ts) — a Remix action that handles all required Shopify webhook topics and validates request authenticityWEBHOOK_NOTES.md— documentation explaining the webhook topics handled, why each is required, and how request authenticity is verified
Input Files
The following file shows the current (incomplete) webhook handler. Extract it before beginning.
=============== FILE: app/routes/webhooks.tsx =============== import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import db from "../db.server";
export const action = async ({ request }: ActionFunctionArgs) => { const { topic, shop, payload } = await authenticate.webhook(request);
switch (topic) { case "APP_UNINSTALLED": await db.session.deleteMany({ where: { shop } }); break; default: throw new Response("Unhandled webhook topic", { status: 404 }); }
return new Response(); }; =============== END FILE ===============
{
"context": "Tests whether the agent correctly upgrades a Shopify app from development-only defaults to production-ready configuration, including persistent session storage, pinned API versioning, and proper App Bridge initialization in the root layout.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Prisma session storage import",
"max_score": 10,
"description": "app/shopify.server.ts imports PrismaSessionStorage from @shopify/shopify-app-session-storage-prisma (not the SQLite variant)"
},
{
"name": "PrismaClient instantiation",
"max_score": 10,
"description": "app/shopify.server.ts creates a PrismaClient instance and passes it to PrismaSessionStorage"
},
{
"name": "SQLite removed",
"max_score": 10,
"description": "app/shopify.server.ts does NOT import or use SQLiteSessionStorage"
},
{
"name": "Pinned API version",
"max_score": 12,
"description": "app/shopify.server.ts uses a specific version string (e.g. \"2025-01\" or similar date-based version) instead of LATEST_API_VERSION for the apiVersion field"
},
{
"name": "LATEST_API_VERSION not in production config",
"max_score": 8,
"description": "app/shopify.server.ts does NOT pass LATEST_API_VERSION as the apiVersion value"
},
{
"name": "AppProvider in root layout",
"max_score": 12,
"description": "app/root.tsx imports and renders AppProvider (from @shopify/shopify-app-remix/react or equivalent Shopify provider)"
},
{
"name": "Root layout wraps Outlet",
"max_score": 8,
"description": "app/root.tsx wraps <Outlet /> inside the AppProvider component"
},
{
"name": "Deployment notes: session storage reason",
"max_score": 10,
"description": "DEPLOYMENT_NOTES.md explains why SQLite is unsuitable for multi-instance deployments (e.g., sessions not shared across instances, file-based storage)"
},
{
"name": "Deployment notes: API version reason",
"max_score": 10,
"description": "DEPLOYMENT_NOTES.md explains why LATEST_API_VERSION is not safe for production (e.g., may introduce breaking changes without notice)"
},
{
"name": "Deployment notes: AppProvider reason",
"max_score": 10,
"description": "DEPLOYMENT_NOTES.md mentions that AppProvider is required for App Bridge context (e.g., useAppBridge returning null without it)"
}
]
}
Migrate Shopify App to Multi-Instance Production Configuration
Problem/Feature Description
The engineering team at a growing e-commerce agency has built a Shopify app that currently works fine on a single developer machine, but they're preparing to launch it publicly on the Shopify App Store. During load testing they discovered that when they scaled their hosting to multiple instances, merchants were getting logged out randomly — their sessions were not persisting across instances.
The current app was prototyped quickly using the default development configuration. Now the team needs to harden the server-side configuration for production: persistent cross-instance session storage, a stable API version that won't silently change on them, and a proper root layout that initializes Shopify's UI framework correctly so merchants don't encounter broken interfaces.
Your job is to write the production-ready server configuration file (app/shopify.server.ts) and the Remix root layout file (app/root.tsx) that addresses these concerns. Also provide a short DEPLOYMENT_NOTES.md explaining what changed from the development setup and why, specifically addressing each production concern.
Output Specification
Produce the following files:
app/shopify.server.ts— the Shopify server configuration module with production-ready session storage and API version settingsapp/root.tsx— the Remix root layout component properly initialized for use in the Shopify AdminDEPLOYMENT_NOTES.md— a brief document explaining the production changes made and why the development defaults are not suitable for multi-instance hosting
Input Files
The following files represent the current development-only configuration. Extract them before beginning.
=============== FILE: app/shopify.server.ts.dev =============== import "@shopify/shopify-app-remix/adapters/node"; import { AppDistribution, DeliveryMethod, shopifyApp, LATEST_API_VERSION, } from "@shopify/shopify-app-remix/server"; import { SQLiteSessionStorage } from "@shopify/shopify-app-session-storage-sqlite";
const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY, apiSecretKey: process.env.SHOPIFY_API_SECRET || "", apiVersion: LATEST_API_VERSION, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL || "", authPathPrefix: "/auth", sessionStorage: new SQLiteSessionStorage("sessions.db"), distribution: AppDistribution.AppStore, webhooks: { APP_UNINSTALLED: { deliveryMethod: DeliveryMethod.Http, callbackUrl: "/webhooks", }, }, hooks: { afterAuth: async ({ session }) => { shopify.registerWebhooks({ session }); }, }, });
export default shopify; export const authenticate = shopify.authenticate; =============== END FILE ===============
=============== FILE: app/root.tsx.dev =============== import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
export default function App() { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> <Outlet /> <ScrollRestoration /> <Scripts /> </body> </html> ); } =============== END FILE ===============
{
"name": "finsi/shopify-app-development",
"version": "0.1.0",
"summary": "Shopify app scaffold with OAuth, App Bridge, and Polaris UI",
"skills": {
"shopify-app-development": {
"path": "SKILL.md"
}
}
}