CommunityRédaction et éditiongithub.com

khadinakbarlabs/shopify-app-builder

Open-source Shopify app development plugin with 32 skills for Claude Code, Codex, Cursor, OpenCode, Gemini CLI, and AI coding agents.

Qu'est-ce que shopify-app-builder ?

shopify-app-builder is a Claude Code agent skill that open-source Shopify app development plugin with 32 skills for Claude Code, Codex, Cursor, OpenCode, Gemini CLI, and AI coding agents.

Compatible avecClaude CodeCodex CLICursorGemini CLIOpenCode
npx skills add khadinakbarlabs/shopify-app-builder

Installed? Explore more Rédaction et édition skills: steipete/notion, affaan-m/seo, affaan-m/brand-voice · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Shopify App Authentication

Shopify provides multiple authentication flows depending on your app type and use case. The modern standard is Token Exchange (2024+) for server-rendered apps, Managed Installation for headless apps, and OAuth 2.0 Authorization Code Grant for legacy/custom implementations. All flows result in an access token for the Shopify GraphQL Admin API.

Authentication Flows Overview

1. Token Exchange (Recommended for 2024+)

Use Case: Server-rendered apps (Remix, Next.js with SSR), Shopify CLI apps Flow: Merchant installs app → Shopify generates temporary exchange token → App exchanges for access token Security: No client secret exposed; uses PKCE-style rotation per request Token Lifetime: Access tokens are short-lived; refresh tokens rotate automatically

Token Exchange Diagram:

1. Merchant clicks "Install" in Shopify Admin
2. Shopify redirects: https://your-app.com/auth/callback?code=EXCHANGE_TOKEN
3. App validates HMAC, exchanges code for access token (private, server-side only)
4. Shopify Admin API grants scopes; token stored in session/database
5. Token auto-refreshes on next request if expired

Remix Implementation (Token Exchange):

// shopify.app.ts (App Configuration)
import { shopifyApp } from '@shopify/shopify-app-remix/server';
import { restResources } from '@shopify/shopify-api/rest/admin/2026-07';
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY || '',
  apiSecret: process.env.SHOPIFY_API_SECRET || '',
  scopes: process.env.SCOPES?.split(',') || [
    'write_products',
    'read_products',
    'write_orders',
    'read_orders',
    'write_customers',
    'read_customers',
    'write_discounts',
    'read_discounts',
    'write_fulfillments',
    'read_fulfillments',
    'write_inventory',
    'read_inventory',
  ],
  appUrl: process.env.SHOPIFY_APP_URL || 'http://localhost:3000',
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
  webhooks: {
    path: '/webhooks',
  },
  isEmbeddedApp: true, // Polaris admin dashboard
  sessionStorage: new PrismaSessionStorage(prisma),
  restResources, // Includes REST API helpers
});

export default shopify;

Environment Variables (.env):

SHOPIFY_API_KEY=your-public-api-key-from-partner-dashboard
SHOPIFY_API_SECRET=your-private-api-secret
SHOPIFY_APP_URL=https://your-domain.ngrok.io  # or prod URL
SCOPES=write_products,read_products,write_orders,read_orders

Auth Routes (routes/auth.$.tsx - Catch-all route):

import { redirect } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { authenticate } = await import('~/shopify.server');
  return authenticate.admin(request); // Token Exchange happens here
};

Callback Handling (routes/auth.callback.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  // Session contains:
  // - session.accessToken (valid for API calls)
  // - session.shop (merchant's shop domain)
  // - session.scope (granted scopes)
  // - session.state (optional custom data)

  return redirect('/app'); // Redirect to dashboard after auth
};

Using Token in API Calls (routes/app.products.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
import { GraphQLClient } from 'graphql-request';

export const loader = async ({ request }) => {
  const { session, admin } = await authenticate.admin(request);

  // Method 1: Use Shopify's admin helper (recommended)
  const response = await admin.graphql(`
    query GetProducts {
      products(first: 10) {
        edges {
          node {
            id
            title
            handle
          }
        }
      }
    }
  `);

  // Method 2: Manual GraphQL call
  const client = new GraphQLClient(
    `https://${session.shop}/admin/api/2026-07/graphql.json`,
    {
      headers: {
        'X-Shopify-Access-Token': session.accessToken,
        'Content-Type': 'application/json',
      },
    }
  );

  const data = await client.request(/* ... */);

  return json({ products: response.data?.products?.edges || [] });
};

Token Refresh (Automatic): Token Exchange tokens auto-refresh via Shopify's session middleware. No manual refresh needed:

// Tokens are refreshed transparently on each request
const response = await admin.graphql(query); // Handles refresh internally

2. Managed Installation (Headless/Custom Apps)

Use Case: Headless storefront, mobile apps, third-party integrations Flow: Merchant authorizes app → Shopify generates permanent access token (no secret rotation) Token Lifetime: Long-lived; no refresh required Security: Token is permanent; store securely in environment variable

Managed Installation Setup:

In Shopify Partner Dashboard:

  1. App Settings > API Credentials
  2. Select "Managed installation" under Admin API access scopes
  3. Merchant grants permission once
  4. Copy access token to your environment
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=example-shop.myshopify.com

Using Managed Installation Token:

import { GraphQLClient } from 'graphql-request';

const client = new GraphQLClient(
  `https://${process.env.SHOPIFY_SHOP_URL}/admin/api/2026-07/graphql.json`,
  {
    headers: {
      'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || '',
    },
  }
);

// Token never expires; call API anytime
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);

3. OAuth 2.0 Authorization Code Grant (Legacy, Still Supported)

Use Case: Public apps with traditional OAuth flow Flow: Merchant clicks "Install" → App redirects to Shopify OAuth → Merchant authorizes → App receives code → App exchanges code for token Token Lifetime: Long-lived access token (no expiration unless revoked) Security: Client secret required; PKCE optional

OAuth Flow Diagram:

1. Merchant visits: https://your-app.com/auth
2. App redirects to: https://your-shop.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://your-app.com/auth/callback&state=RANDOM
3. Merchant authorizes app in Shopify Admin
4. Shopify redirects back: https://your-app.com/auth/callback?code=AUTHORIZATION_CODE&hmac=SIGNATURE&state=RANDOM&shop=your-shop.myshopify.com
5. App validates HMAC and state
6. App exchanges code for token (server-side, using client secret)
7. App stores token in database

Express.js OAuth Example:

import express from 'express';
import axios from 'axios';
import crypto from 'crypto';

const app = express();

const API_KEY = process.env.SHOPIFY_API_KEY || '';
const API_SECRET = process.env.SHOPIFY_API_SECRET || '';
const REDIRECT_URI = process.env.REDIRECT_URI || 'https://your-app.com/auth/callback';
const SCOPES = 'write_products,read_products';

// Step 1: Redirect merchant to Shopify OAuth
app.get('/auth', (req, res) => {
  const shop = req.query.shop as string;

  if (!shop || !shop.includes('.myshopify.com')) {
    return res.status(400).send('Missing or invalid shop parameter');
  }

  const state = crypto.randomBytes(16).toString('hex');
  const nonce = crypto.randomBytes(16).toString('hex');

  // Store state in session (or database) for validation
  req.session.state = state;
  req.session.nonce = nonce;

  const authUrl = new URL(
    `/admin/oauth/authorize`,
    `https://${shop}`
  );
  authUrl.searchParams.append('client_id', API_KEY);
  authUrl.searchParams.append('scope', SCOPES);
  authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.append('state', state);

  res.redirect(authUrl.toString());
});

// Step 2: Handle OAuth callback
app.get('/auth/callback', async (req, res) => {
  const { code, hmac, shop, state } = req.query;

  // Validate HMAC
  const message = Object.entries(req.query)
    .filter(([key]) => key !== 'hmac')
    .map(([key, value]) => `${key}=${value}`)
    .sort()
    .join('&');

  const hash = crypto
    .createHmac('sha256', API_SECRET)
    .update(message, 'utf8')
    .digest('base64');

  if (hash !== hmac) {
    return res.status(401).send('Unauthorized request detected');
  }

  // Validate state
  if (state !== req.session.state) {
    return res.status(401).send('State mismatch');
  }

  try {
    // Exchange code for access token
    const response = await axios.post(
      `https://${shop}/admin/oauth/access_token`,
      {
        client_id: API_KEY,
        client_secret: API_SECRET,
        code,
      }
    );

    const { access_token, scope } = response.data;

    // Store access token (in database, not session, for persistence)
    await storeAccessToken(shop as string, access_token, scope);

    // Redirect to app dashboard
    res.redirect(`/app?shop=${shop}`);
  } catch (error) {
    console.error('Token exchange error:', error);
    res.status(500).send('Authentication failed');
  }
});

// Helper function to store token
async function storeAccessToken(shop: string, token: string, scope: string) {
  // Store in database (example using mock storage)
  const db = {
    shops: {} as Record<string, { token: string; scope: string }>,
  };
  db.shops[shop] = { token, scope };
}

app.listen(3000);

Session Storage Adapters

Access tokens must be stored persistently. Shopify provides adapters for common storage backends:

Prisma (Recommended for Remix)

Schema (prisma/schema.prisma):

model Session {
  id        String    @id
  shop      String
  state     String
  isOnline  Boolean   @default(false)
  accessToken String
  refreshToken String?
  scope     String
  expiresAt DateTime?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@unique([shop, state])
  @@index([shop])
}

Setup (shopify.app.ts):

import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  // ...
  sessionStorage: new PrismaSessionStorage(prisma),
});

Redis

import { RedisSessionStorage } from '@shopify/shopify-app-session-storage-redis';
import redis from 'redis';

const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379,
});

const sessionStorage = new RedisSessionStorage({
  client: redisClient,
  prefix: 'shopify_session:',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

In-Memory (Development Only)

import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';

const sessionStorage = new MemorySessionStorage();

const shopify = shopifyApp({
  // ...
  sessionStorage, // CAUTION: Sessions lost on app restart; development only
});

DynamoDB

import { DynamoDBSessionStorage } from '@shopify/shopify-app-session-storage-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const dynamoDBClient = new DynamoDBClient({ region: 'us-east-1' });

const sessionStorage = new DynamoDBSessionStorage({
  client: dynamoDBClient,
  tableName: 'shopify-sessions',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

Online vs. Offline Tokens

Online Token:

  • Scope: Current user's permissions (typically admin user)
  • Expiration: 24 hours
  • Use Case: Browser-based actions (Polaris admin dashboard)
  • Limitations: Cannot run background jobs; limited when user logs out

Offline Token:

  • Scope: App's granted scopes
  • Expiration: None; permanent until revoked
  • Use Case: Background jobs, webhooks, scheduled tasks
  • Limitations: None; use by default for app operations

Requesting Offline Token in Token Exchange:

// shopify.app.ts
const shopify = shopifyApp({
  // ...
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
});

// Remix automatically requests offline token by default
// No action needed; use session.accessToken for API calls

Using Offline Token for Webhooks:

// webhooks/products-update.ts
export const webhooks = {
  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/app-uninstalled',
  },
  PRODUCTS_UPDATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/products-update',
  },
};

export default defineWebhooksConfig(
  async (request, { admin, session }) => {
    const { body, query } = await graphql.query(request, {
      query: GET_PRODUCT,
      variables: { id: 'gid://shopify/Product/123' },
    });

    // session.accessToken is offline token; valid here
    console.log(`Webhook processed with token for shop: ${session.shop}`);
  },
  webhooksConfig
);

App Proxy Authentication

Use Case: Storefront (public-facing) requests to app backend Authentication: HMAC signature validation (like webhooks) Flow: Storefront → Liquid proxy request → App backend (validates HMAC) → Response

Setting Up App Proxy (shopify.app.toml):

[[extensions]]
type = "app_proxy"
name = "Storefront API"
url = "/api/proxy"
subpath = "loyalty"  # Requests to /apps/loyalty/* are routed here

App Proxy Handler (Remix routes/api/proxy.ts):

import { json } from '@shopify/remix-oxygen';
import crypto from 'crypto';

export const loader = async ({ request }) => {
  const url = new URL(request.url);
  const hmac = url.searchParams.get('hmac') || '';
  const timestamp = url.searchParams.get('_t') || '';
  const shop = url.searchParams.get('shop') || '';

  // Build message to validate HMAC
  const params = new URLSearchParams();
  Array.from(url.searchParams.entries()).forEach(([key, value]) => {
    if (key !== 'hmac') params.append(key, value);
  });

  const message = params.toString();
  const hash = crypto
    .createHmac('sha256', process.env.SHOPIFY_API_SECRET || '')
    .update(message, 'utf8')
    .digest('base64');

  if (hash !== hmac) {
    return json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Validate timestamp (within 24 hours)
  const requestTime = parseInt(timestamp, 10);
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - requestTime) > 86400) {
    return json({ error: 'Request expired' }, { status: 401 });
  }

  // Valid app proxy request; return customer's loyalty points
  const customerId = url.searchParams.get('customer_id') || '';
  const points = await getLoyaltyPoints(customerId);

  return json({ points });
};

async function getLoyaltyPoints(customerId: string) {
  // Fetch from database
  return 1500; // Example
}

Storefront Liquid Snippet:

<div id="loyalty-widget">
  <p>Your loyalty points: <span id="points">Loading...</span></p>
</div>

<script>
fetch('/apps/loyalty?customer_id={{ customer.id }}')
  .then(r => r.json())
  .then(data => {
    document.getElementById('points').textContent = data.points;
  });
</script>

Webhook Signature Verification

How It Works: Shopify sends HMAC-SHA256 signature in X-Shopify-Hmac-SHA256 header

Verify Signature (Manual):

import crypto from 'crypto';

export const verifyWebhookSignature = (
  request: Request,
  secret: string
): boolean => {
  const hmacHeader = request.headers.get('X-Shopify-Hmac-SHA256') || '';
  const body = request.body; // Must be raw bytes, not JSON

  const hash = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(hash),
    Buffer.from(hmacHeader)
  );
};

Verify Signature (Remix Shopify Package):

import { authenticate } from '~/shopify.server';

export const action = async ({ request }) => {
  const { webhook } = await authenticate.webhook(request);

  // Signature already validated by middleware
  console.log(`Webhook received for shop: ${webhook.shop}`);
  console.log(`Topic: ${webhook.topic}`);
  console.log(`Body:`, webhook.payload);

  return json({ status: 'ok' });
};

Register Webhook (shopify.app.ts):

const shopify = shopifyApp({
  // ...
  webhooks: {
    path: '/webhooks',
    validateHmac: true, // Automatic signature verification
  },
});

// Define webhooks in routes/webhooks.ts
export const webhooks = {
  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/app-uninstalled',
  },
  ORDERS_CREATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/orders-create',
  },
};

Customer Account API Authentication

Use Case: Access customer account data (orders, addresses, metafields) Authentication: Customer-specific access tokens (from Shopify Hydrogen or customer flow) Note: Different from admin API; limited to customer data only

Get Customer Access Token (in Hydrogen/Storefront):

// This is typically handled by Shopify's customer auth flow
const customerAccessToken = 'shpuc_XXXXX'; // Provided by auth

const response = await fetch(
  `https://example-shop.myshopify.com/api/2026-07/graphql.json`,
  {
    method: 'POST',
    headers: {
      'X-Shopify-Storefront-Access-Token': 'public-storefront-token',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: `
        query {
          customer(customerAccessToken: "${customerAccessToken}") {
            id
            firstName
            email
            orders(first: 10) {
              edges {
                node {
                  id
                  orderNumber
                  totalPrice
                }
              }
            }
          }
        }
      `,
    }),
  }
);

Public vs. Custom vs. Custom Distribution Apps

Public App (Shopify App Store):

  • Listed in Shopify App Store
  • OAuth 2.0 or Token Exchange
  • Scopes reviewed by Shopify
  • Available to all merchants
  • Example: "Email Marketing Pro"

Custom App (Internal Use):

  • Private; not listed in App Store
  • No scopes; request all access
  • Created by/for single merchant
  • Only accessible to merchant account
  • Example: Custom inventory sync for specific store

Custom Distribution App:

  • Limited distribution; only shared with specific merchants via link
  • Behaves like public app (listed in custom store) but not publicly visible
  • OAuth 2.0 required
  • Scopes still reviewed

Configuration (shopify.app.toml):

# Public App
scopes = "write_products,read_products,write_orders"
distribution = "public"

# Custom App (all scopes by default)
distribution = "private"

# Custom Distribution
distribution = "custom"
allowedDomains = ["company-partner.myshopify.com"]

Top 10 Authentication Bugs & Fixes

BugSymptomFix
Missing HMAC validationWebhook spoofing; malicious requests processedAlways validate HMAC signature in webhook handlers; use authenticate.webhook(request)
Storing access token in session cookieToken exposed in browser; XSS vulnerabilityStore token in database (Prisma); never send to client; use httpOnly cookies for session ID only
Expired token not refreshed"Unauthorized" errors after 24h (online tokens)Token Exchange auto-refreshes; OAuth tokens are permanent; Managed Installation tokens never expire
Incorrect HMAC secret"Invalid signature" errors on valid requestsUse correct SHOPIFY_API_SECRET; verify in Partner Dashboard > App Settings
State parameter not validatedCSRF attacks; attacker redirects merchantStore state in session; validate in callback; use crypto.randomBytes(16).toString('hex') for state generation
App proxy timestamp not checkedOld requests replayed; business logic executed twiceValidate timestamp within 24h; use Math.abs(currentTime - timestamp) < 86400
Scope creep (requesting too many scopes)App rejected by Shopify; merchant distrustRequest only scopes needed; remove unused scopes from SCOPES array
Token stored in environment variable for multi-tenantSecurity breach; one merchant's token accessed by anotherUse database storage (Prisma/Redis); one token per shop; index by shop domain
Webhook signature verified but not in constant-timeTiming attacks; signature can be guessedUse crypto.timingSafeEqual() for comparison; avoid simple ===
Customer access token hardcodedExposed in source code; customer data accessedNever hardcode tokens; pass via environment variables or customer auth flow

Full Working Examples

Example 1: Remix Token Exchange App (Complete)

Directory Structure:

my-app/
├── app/
│   ├── routes/
│   │   ├── auth.$.tsx (Auth handler)
│   │   ├── auth.callback.tsx (Callback)
│   │   └── app.products.tsx (Protected route)
│   ├── shopify.server.ts (Config)
│   └── db.server.ts (Prisma client)
├── prisma/
│   ├── schema.prisma
│   └── migrations/
├── .env (API keys)
└── shopify.app.toml

shopify.app.toml:

scopes = "write_products,read_products,write_orders,read_orders"

app/shopify.server.ts:

import { shopifyApp } from '@shopify/shopify-app-remix/server';
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

export const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecret: process.env.SHOPIFY_API_SECRET,
  scopes: process.env.SCOPES?.split(',') || [],
  appUrl: process.env.SHOPIFY_APP_URL,
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
  webhooks: {
    path: '/webhooks',
  },
  isEmbeddedApp: true,
  sessionStorage: new PrismaSessionStorage(prisma),
});

export const authenticate = shopify.authenticate;

app/routes/auth.$.tsx:

import { redirect } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  await authenticate.admin(request);
  return redirect('/app');
};

app/routes/auth.callback.tsx:

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  return redirect(`/app?shop=${session.shop}`);
};

app/routes/app.products.tsx:

import { json } from '@shopify/remix-oxygen';
import { useLoaderData } from '@remix-run/react';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { admin } = await authenticate.admin(request);

  const response = await admin.graphql(`
    query GetProducts {
      products(first: 10) {
        edges {
          node {
            id
            title
          }
        }
      }
    }
  `);

  const products = response.data?.products?.edges || [];

  return json({ products });
};

export default function Products() {
  const { products } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map((p) => (
          <li key={p.node.id}>{p.node.title}</li>
        ))}
      </ul>
    </div>
  );
}

Example 2: Webhook Signature Verification

routes/webhooks.ts:

import { define } from '@shopify/shopify-app-remix/server';
import { DeliveryMethod } from '@shopify/shopify-api';

export const webhooks = define({
  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/app-uninstalled',
  },
  PRODUCTS_CREATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/products-create',
  },
  PRODUCTS_UPDATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/products-update',
  },
});

export async function handleWebhook(
  topic,
  shop,
  body,
  webhookId
) {
  switch (topic) {
    case 'app/uninstalled':
      await deleteShopData(shop);
      break;
    case 'products/create':
      await syncProduct(shop, body);
      break;
    case 'products/update':
      await updateProduct(shop, body);
      break;
  }
}

async function deleteShopData(shop: string) {
  // Clean up shop data on uninstall
  console.log(`App uninstalled for shop: ${shop}`);
}

async function syncProduct(shop: string, body: any) {
  const product = JSON.parse(body).product;
  console.log(`Product created in ${shop}: ${product.title}`);
}

async function updateProduct(shop: string, body: any) {
  const product = JSON.parse(body).product;
  console.log(`Product updated in ${shop}: ${product.title}`);
}

routes/webhooks/app-uninstalled.tsx:

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
import { deleteShopData } from '~/models/shop.server';

export const action = async ({ request }) => {
  const { webhook } = await authenticate.webhook(request);

  // HMAC signature already validated
  await deleteShopData(webhook.shop);

  return json({ status: 'processed' });
};

Example 3: App Proxy with Customer Loyalty

routes/api/proxy.tsx:

import { json } from '@shopify/remix-oxygen';
import crypto from 'crypto';

export const loader = async ({ request }) => {
  const url = new URL(request.url);
  const hmac = url.searchParams.get('hmac') || '';
  const timestamp = url.searchParams.get('_t') || '';

  // Build message for HMAC validation
  const params = new URLSearchParams();
  Array.from(url.searchParams.entries()).forEach(([key, value]) => {
    if (key !== 'hmac') params.append(key, value);
  });

  const message = params.toString();
  const hash = crypto
    .createHmac('sha256', process.env.SHOPIFY_API_SECRET || '')
    .update(message, 'utf8')
    .digest('base64');

  // Validate signature
  if (hash !== hmac) {
    return json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Validate timestamp
  const requestTime = parseInt(timestamp, 10);
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - requestTime) > 86400) {
    return json({ error: 'Request expired' }, { status: 401 });
  }

  // Valid request; return loyalty data
  const customerId = url.searchParams.get('customer_id') || '';
  const loyaltyPoints = await getLoyaltyPoints(customerId);

  return json({ loyaltyPoints, success: true });
};

async function getLoyaltyPoints(customerId: string): Promise<number> {
  // Fetch from database
  return 1500;
}

API Version & Scope Reference

Current when this release was audited: 2026-07. Verify Shopify's latest stable version before deployment.

Essential Scopes:

  • write_products, read_products — Manage product catalog
  • write_orders, read_orders — Access order data
  • write_customers, read_customers — Manage customer data
  • write_fulfillments, read_fulfillments — Manage fulfillments
  • write_inventory, read_inventory — Manage inventory levels
  • write_discounts, read_discounts — Create/manage discounts
  • write_draft_orders, read_draft_orders — Draft order management
  • write_checkout, read_checkout — Checkout customization
  • write_metafields, read_metafields — Manage custom data

Scope Review: Scopes are reviewed by Shopify during app approval. Request only necessary scopes.

Resources

Skills associés

steipete/notion

Notion CLI/API for pages, Markdown content, data sources, files, comments, search, Workers, and raw API calls.

community

affaan-m/seo

Audit, plan, and implement SEO improvements across technical SEO, on-page optimization, structured data, Core Web Vitals, and content strategy. Use when the user wants better search visibility, SEO remediation, schema markup, sitemap/robots work, or keyword mapping.

community

affaan-m/brand-voice

Build a source-derived writing style profile from real posts, essays, launch notes, docs, or site copy, then reuse that profile across content, outreach, and social workflows. Use when the user wants voice consistency without generic AI writing tropes.

community

affaan-m/crosspost

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

community

affaan-m/x-api

X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.

community

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

community