shopify-app-development
Build embedded Shopify apps using the Remix framework, App Bridge for UI integration, Polaris components, and OAuth authentication flow
What this skill does
# 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**
```bash
npm install -g @shopify/cli @shopify/theme
shopify app init my-shopify-app
# Choose: Remix template
cd my-shopify-app
shopify app dev
```
This 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`:
```typescript
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`:
```typescript
// 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**
```typescript
// 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`:
```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**
```bash
shopify app deploy
# Deploys to Shopify (functions/extensions)
# Deploy the Remix server separately (Fly.io, Railway, Render)
fly launch
fly deploy
```
## Examples
### Mutation via Admin GraphQL API
```typescript
// 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)
```typescript
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.location` navigation 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 `LARelated in platform-shopify
shopify-metafields
IncludedStore custom data on any Shopify resource — products, orders, customers — using typed metafield definitions accessible from Liquid and the Storefront API
shopify-theme-development
IncludedBuild and customize Shopify themes using Liquid templating, JSON sections, dynamic blocks, and theme app extensions for added functionality
shopify-admin-api
IncludedAutomate Shopify store operations — products, orders, inventory, and customers — using the GraphQL Admin API with bulk operation support
shopify-storefront-api
IncludedBuild a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK
shopify-webhooks
IncludedRegister, verify, and reliably process Shopify webhook events for orders, inventory, and customers with HMAC validation and idempotency handling
shopify-checkout-extensions
IncludedCustomize Shopify's checkout with UI extensions for upsells and custom fields, plus Shopify Functions for serverless discount and shipping logic