CommunityRédaction et éditiongithub.com

khadinakbarlabs/shopify-app-builder

Plugin open-source pour développer des applications Shopify avec 32 compétences pour Claude Code, Codex, Cursor, OpenCode, Gemini CLI et agents de codage IA.

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

Ce plugin open-source pour agents de codage IA (Claude Code, Cursor, Codex, etc.) fournit 32 compétences dédiées au développement d'applications Shopify. Il couvre l'intégralité des flows d'authentification modernes (Token Exchange 2024+, Managed Installation, OAuth 2.0), la gestion des tokens en ligne/hors ligne, l'implémentation d'adaptateurs de stockage de session (Prisma, Redis, DynamoDB) et les webhooks. Le plugin génère des extraits de code prêts à l'emploi en TypeScript pour les frameworks comme Remix ou Express.js, et inclut des exemples concrets de configuration d'environnement, de routes d'authentification et d'appels API GraphQL. Idéal pour les développeurs Shopify qui veulent accélérer la création d'apps sécurisées et scalables avec l'aide d'un agent IA.

Compatible avec✓Claude Code✓Codex CLI✓Cursor✓Gemini CLI✓OpenCode
npx skills add khadinakbarlabs/shopify-app-builder

Installed? Explore more Rédaction et édition skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · 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

Authentification d'Application Shopify

Shopify propose plusieurs flux d'authentification selon le type d'application et le cas d'usage. Le standard moderne est Token Exchange (2024+) pour les applications rendues côté serveur, Managed Installation pour les applications headless, et OAuth 2.0 Authorization Code Grant pour les implémentations legacy/personnalisées. Tous les flux aboutissent à un jeton d'accès pour l'API GraphQL Admin de Shopify.

Aperçu des Flux d'Authentification

1. Token Exchange (Recommandé pour 2024+)

Cas d'Usage : Applications rendues côté serveur (Remix, Next.js avec SSR), applications Shopify CLI Flux : Le commerçant installe l'application → Shopify génère un jeton d'échange temporaire → L'application l'échange contre un jeton d'accès Sécurité : Aucun secret client exposé ; utilise une rotation de type PKCE par requête Durée de Vie du Jeton : Les jetons d'accès sont de courte durée ; les jetons de rafraîchissement tournent automatiquement

Diagramme Token Exchange :

1. Le commerçant clique sur "Installer" dans l'Admin Shopify
2. Shopify redirige : https://votre-app.com/auth/callback?code=EXCHANGE_TOKEN
3. L'application valide le HMAC, échange le code contre un jeton d'accès (côté serveur privé uniquement)
4. L'API Admin Shopify accorde les portées ; le jeton stocké en session/base de données
5. Le jeton s'auto-rafraîchit à la prochaine requête s'il est expiré

Implémentation Remix (Token Exchange) :

// shopify.app.ts (Configuration de l'application)
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, // Tableau de bord Polaris
  sessionStorage: new PrismaSessionStorage(prisma),
  restResources, // Inclut les aides de l'API REST
});

export default shopify;

Variables d'Environnement (.env) :

SHOPIFY_API_KEY=votre-cle-api-publique-du-tableau-de-bord-partenaire
SHOPIFY_API_SECRET=votre-secret-api-prive
SHOPIFY_APP_URL=https://votre-domaine.ngrok.io  # ou URL de production
SCOPES=write_products,read_products,write_orders,read_orders

Routes d'Authentification (routes/auth.$.tsx - Route catch-all) :

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 s'effectue ici
};

Gestion du Callback (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);
  // La session contient :
  // - session.accessToken (valide pour les appels API)
  // - session.shop (domaine de la boutique du commerçant)
  // - session.scope (portées accordées)
  // - session.state (données personnalisées optionnelles)

  return redirect('/app'); // Rediriger vers le tableau de bord après authentification
};

Utilisation du Jeton dans les Appels API (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);

  // Méthode 1 : Utiliser l'aide admin de Shopify (recommandé)
  const response = await admin.graphql(`
    query GetProducts {
      products(first: 10) {
        edges {
          node {
            id
            title
            handle
          }
        }
      }
    }
  `);

  // Méthode 2 : Appel GraphQL manuel
  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 || [] });
};

Rafraîchissement du Jeton (Automatique) : Les jetons Token Exchange se rafraîchissent automatiquement via le middleware de session de Shopify. Aucun rafraîchissement manuel nécessaire :

// Les jetons sont rafraîchis de manière transparente à chaque requête
const response = await admin.graphql(query); // Gère le rafraîchissement en interne

2. Managed Installation (Applications Headless/Personnalisées)

Cas d'Usage : Storefront headless, applications mobiles, intégrations tierces Flux : Le commerçant autorise l'application → Shopify génère un jeton d'accès permanent (pas de rotation de secret) Durée de Vie du Jeton : Longue durée ; aucun rafraîchissement requis Sécurité : Le jeton est permanent ; à stocker en toute sécurité dans une variable d'environnement

Configuration Managed Installation :

Dans le Tableau de Bord Partenaire Shopify :

  1. Paramètres de l'application > Identifiants API
  2. Sélectionner "Managed installation" sous les portées d'accès à l'API Admin
  3. Le commerçant accorde l'autorisation une fois
  4. Copier le jeton d'accès dans votre environnement
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=exemple-boutique.myshopify.com

Utilisation du Jeton Managed Installation :

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 || '',
    },
  }
);

// Le jeton n'expire jamais ; appelez l'API à tout moment
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);

3. OAuth 2.0 Authorization Code Grant (Legacy, Toujours Supporté)

Cas d'Usage : Applications publiques avec flux OAuth traditionnel Flux : Le commerçant clique sur "Installer" → L'application redirige vers Shopify OAuth → Le commerçant autorise → L'application reçoit un code → L'application échange le code contre un jeton Durée de Vie du Jeton : Jeton d'accès longue durée (pas d'expiration sauf révocation) Sécurité : Secret client requis ; PKCE optionnel

Diagramme du Flux OAuth :

1. Le commerçant visite : https://votre-app.com/auth
2. L'application redirige vers : https://votre-boutique.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://votre-app.com/auth/callback&state=RANDOM
3. Le commerçant autorise l'application dans l'Admin Shopify
4. Shopify redirige vers : https://votre-app.com/auth/callback?code=AUTHORIZATION_CODE&hmac=SIGNATURE&state=RANDOM&shop=votre-boutique.myshopify.com
5. L'application valide le HMAC et l'état
6. L'application échange le code contre un jeton (côté serveur, en utilisant le secret client)
7. L'application stocke le jeton dans la base de données

Exemple Express.js OAuth :

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://votre-app.com/auth/callback';
const SCOPES = 'write_products,read_products';

// Étape 1 : Rediriger le commerçant vers 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('Paramètre shop manquant ou invalide');
  }

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

  // Stocker l'état dans la session (ou base de données) pour 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());
});

// Étape 2 : Gérer le callback OAuth
app.get('/auth/callback', async (req, res) => {
  const { code, hmac, shop, state } = req.query;

  // Valider le 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('Requête non autorisée détectée');
  }

  // Valider l'état
  if (state !== req.session.state) {
    return res.status(401).send('Non-concordance de l\'état');
  }

  try {
    // Échanger le code contre un jeton d'accès
    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;

    // Stocker le jeton d'accès (dans la base de données, pas la session, pour persistance)
    await storeAccessToken(shop as string, access_token, scope);

    // Rediriger vers le tableau de bord de l'application
    res.redirect(`/app?shop=${shop}`);
  } catch (error) {
    console.error('Erreur d\'échange de jeton :', error);
    res.status(500).send('Authentification échouée');
  }
});

// Fonction d'aide pour stocker le jeton
async function storeAccessToken(shop: string, token: string, scope: string) {
  // Stocker dans la base de données (exemple avec stockage mock)
  const db = {
    shops: {} as Record<string, { token: string; scope: string }>,
  };
  db.shops[shop] = { token, scope };
}

app.listen(3000);

Adaptateurs de Stockage de Session

Les jetons d'accès doivent être stockés de manière persistante. Shopify fournit des adaptateurs pour les backends de stockage courants :

Prisma (Recommandé pour Remix)

Schéma (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])
}

Configuration (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,
});

En Mémoire (Développement Uniquement)

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

const sessionStorage = new MemorySessionStorage();

const shopify = shopifyApp({
  // ...
  sessionStorage, // ATTENTION : Sessions perdues au redémarrage de l'application ; développement uniquement
});

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,
});

Jetons en Ligne vs. Hors Ligne

Jeton en Ligne :

  • Portée : Autorisations de l'utilisateur actuel (généralement admin)
  • Expiration : 24 heures
  • Cas d'Usage : Actions basées sur le navigateur (tableau de bord Polaris)
  • Limitations : Ne peut pas exécuter de tâches en arrière-plan ; limité lorsque l'utilisateur se déconnecte

Jeton Hors Ligne :

  • Portée : Portées accordées à l'application
  • Expiration : Aucune ; permanent jusqu'à révocation
  • Cas d'Usage : Tâches en arrière-plan, webhooks, tâches planifiées
  • Limitations : Aucune ; à utiliser par défaut pour les opérations de l'application

Demande de Jeton Hors Ligne dans Token Exchange :

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

// Remix demande automatiquement le jeton hors ligne par défaut
// Aucune action nécessaire ; utilisez session.accessToken pour les appels API

Utilisation du Jeton Hors Ligne pour les 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 est le jeton hors ligne ; valide ici
    console.log(`Webhook traité avec le jeton pour la boutique : ${session.shop}`);
  },
  webhooksConfig
);

Authentification par Proxy d'Application

Cas d'Usage : Requêtes Storefront (publiques) vers le backend de l'application Authentification : Validation de signature HMAC (comme les webhooks) Flux : Storefront → Requête proxy Liquid → Backend de l'application (valide HMAC) → Réponse

Configuration du Proxy d'Application (shopify.app.toml) :

[[extensions]]
type = "app_proxy"
name = "API Storefront"
url = "/api/proxy"
subpath = "loyalty"  # Les requêtes vers /apps/loyalty/* sont routées ici

Gestionnaire de Proxy d'Application (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') || '';

  // Construction et validation HMAC...
  // ...

  return json({ data: 'Réponse du proxy d\'application' });
};

Skills associés