### Install Shopify CLI Globally (pnpm) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Install the latest version of the Shopify CLI globally using pnpm. ```bash pnpm add -g @shopify/cli@latest ``` -------------------------------- ### Start Local Development Server Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Run the Shopify CLI development command to start your local development server. This command handles authentication, environment variables, and tunneling. ```shell shopify app dev ``` -------------------------------- ### Install Shopify CLI Globally (yarn) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Install the latest version of the Shopify CLI globally using yarn. ```bash yarn global add @shopify/cli@latest ``` -------------------------------- ### Install Shopify CLI Globally (npm) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Install the latest version of the Shopify CLI globally using npm. ```bash npm install -g @shopify/cli@latest ``` -------------------------------- ### Shopify TOML Configuration for MongoDB Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Configure the `shopify.web.toml` file with predev and dev commands when using MongoDB. This setup uses `prisma db push` instead of `prisma migrate` and starts the Remix development server. ```toml [commands] predev = "npx prisma generate && npx prisma db push" dev = "npm exec remix vite:dev" ``` -------------------------------- ### Authenticate Admin and Query Products Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md This example demonstrates how to authenticate an admin request and query product data using the Shopify API. Ensure the `shopify` object is imported from `/app/shopify.server.js`. ```javascript export async function loader({ request }) { const { admin } = await shopify.authenticate.admin(request); const response = await admin.graphql( ` { products(first: 25) { nodes { title description } } }` ); const { data: { products: { nodes }, }, } = await response.json(); return nodes; } ``` -------------------------------- ### Update build and start scripts Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Modify the 'build' and 'start' scripts in package.json to use Vite for building and serving the Remix app. ```diff "scripts": { - "build": "tsc && remix build", - "start": "remix-serve build/index.js", + "build": "vite build && vite build --ssr", + "start": "remix-serve ./build/server/index.js", }, ``` -------------------------------- ### Install Node.js 20 using NVM Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Install and use Node.js version 20.17 using the Node Version Manager (NVM). ```bash nvm install 20.17 nvm use 20.17 ``` -------------------------------- ### Initialize Shopify App Instance with Remix Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Configures the Shopify integration using `shopifyApp()`. This setup includes API keys, version, scopes, URL, authentication paths, session storage, distribution settings, and future configurations like token exchange and rotation. It also handles custom shop domains. ```typescript import "@shopify/shopify-app-remix/adapters/node"; import { ApiVersion, AppDistribution, shopifyApp, } from "@shopify/shopify-app-remix/server"; import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma"; import prisma from "./db.server"; const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY, apiSecretKey: process.env.SHOPIFY_API_SECRET || "", apiVersion: ApiVersion.January25, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL || "", authPathPrefix: "/auth", sessionStorage: new PrismaSessionStorage(prisma), distribution: AppDistribution.AppStore, future: { unstable_newEmbeddedAuthStrategy: true, expiringOfflineAccessTokens: true, }, ...(process.env.SHOP_CUSTOM_DOMAIN ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] } : {}), }); export default shopify; // Granular named exports for use in routes export const apiVersion = ApiVersion.January25; export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders; export const authenticate = shopify.authenticate; export const unauthenticated = shopify.unauthenticated; export const login = shopify.login; export const registerWebhooks = shopify.registerWebhooks; export const sessionStorage = shopify.sessionStorage; ``` -------------------------------- ### Install Vite Dependencies (yarn) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Add Vite and vite-tsconfig-paths as development dependencies using yarn. ```bash yarn add --dev vite yarn add vite-tsconfig-paths ``` -------------------------------- ### Install Vite Dependencies (pnpm) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Add Vite and vite-tsconfig-paths as development dependencies using pnpm. ```bash pnpm add --save-dev vite pnpm add vite-tsconfig-paths ``` -------------------------------- ### Install Vite Dependencies (npm) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Add Vite and vite-tsconfig-paths as development dependencies using npm. ```bash npm add --save-dev vite npm add vite-tsconfig-paths ``` -------------------------------- ### Configure Vite Build for Shopify App Template Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Configures HMR tunnelling, CORS for Shopify CLI tunnel, and Remix v3 future flags. Pre-bundles Shopify UI packages for fast cold starts. ```typescript // vite.config.ts import { vitePlugin as remix } from "@remix-run/dev"; import { installGlobals } from "@remix-run/node"; import { defineConfig, type UserConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; installGlobals({ nativeFetch: true }); // Normalise HOST → SHOPIFY_APP_URL (Shopify CLI compat workaround) if (process.env.HOST && (!process.env.SHOPIFY_APP_URL || process.env.SHOPIFY_APP_URL === process.env.HOST)) { process.env.SHOPIFY_APP_URL = process.env.HOST; delete process.env.HOST; } const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost").hostname; export default defineConfig({ server: { allowedHosts: [host], cors: { preflightContinue: true }, port: Number(process.env.PORT || 3000), hmr: host === "localhost" ? { protocol: "ws", host: "localhost", port: 64999, clientPort: 64999 } : { protocol: "wss", host, port: parseInt(process.env.FRONTEND_PORT!) || 8002, clientPort: 443 }, fs: { allow: ["app", "node_modules"] }, }, plugins: [ remix({ ignoredRouteFiles: ["**/.*"], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, v3_singleFetch: false, // Disabled for Shopify compatibility v3_routeConfig: true, }, }), tsconfigPaths(), ], build: { assetsInlineLimit: 0 }, optimizeDeps: { include: ["@shopify/app-bridge-react", "@shopify/polaris"], }, }) satisfies UserConfig; ``` -------------------------------- ### Authenticate Admin Route Loader/Action Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Use `authenticate.admin(request)` at the top of protected loader or action functions. It validates the request against the session and automatically redirects to OAuth if the app is not installed. It returns an `admin` client for making Shopify Admin API GraphQL calls. ```typescript import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; // Loader: guard the route (redirects to OAuth if unauthenticated) export const loader = async ({ request }: LoaderFunctionArgs) => { await authenticate.admin(request); return null; }; // Action: authenticate and use the admin GraphQL client export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); // 1. Create a product const color = ["Red", "Orange", "Yellow", "Green"][Math.floor(Math.random() * 4)]; const createResponse = await admin.graphql( `#graphql mutation populateProduct($product: ProductCreateInput!) { productCreate(product: $product) { product { id title handle status variants(first: 10) { edges { node { id price barcode createdAt } } } } } }`, { variables: { product: { title: `${color} Snowboard` } } }, ); const { data } = await createResponse.json(); const product = data!.productCreate!.product!; const variantId = product.variants.edges[0]!.node!.id!; // 2. Update the variant price const updateResponse = await admin.graphql( `#graphql mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id price barcode createdAt } } }`, { variables: { productId: product.id, variants: [{ id: variantId, price: "100.00" }] } }, ); const variantData = await updateResponse.json(); return { product: data!.productCreate!.product, variant: variantData!.data!.productVariantsBulkUpdate!.productVariants, }; // Expected shape: // { // product: { id: "gid://shopify/Product/12345", title: "Red Snowboard", handle: "red-snowboard", status: "ACTIVE", variants: {...} }, // variant: [{ id: "gid://shopify/ProductVariant/67890", price: "100.00", barcode: null, createdAt: "2025-01-01T00:00:00Z" }] // } }; ``` -------------------------------- ### authenticate.admin(request) Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Validates the incoming request against the stored session for admin routes. It automatically triggers the OAuth redirect if the merchant has not installed the app yet. Returns an `admin` client pre-configured with the shop's access token for making GraphQL calls to the Shopify Admin API. This function must be called at the top of every protected loader and action. ```APIDOC ## `authenticate.admin(request)` — Authenticate an admin route loader or action Validates the incoming request against the stored session. If the merchant has not installed the app yet, it triggers the OAuth redirect automatically. Returns an `admin` client pre-configured with the shop's access token, ready to make GraphQL calls against the Shopify Admin API. Must be called at the top of every protected loader/action. ```typescript // app/routes/app._index.tsx — loader + action demonstrating authenticate.admin import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; // Loader: guard the route (redirects to OAuth if unauthenticated) export const loader = async ({ request }: LoaderFunctionArgs) => { await authenticate.admin(request); return null; }; // Action: authenticate and use the admin GraphQL client export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); // 1. Create a product const color = ["Red", "Orange", "Yellow", "Green"][Math.floor(Math.random() * 4)]; const createResponse = await admin.graphql( `#graphql mutation populateProduct($product: ProductCreateInput!) { productCreate(product: $product) { product { id title handle status variants(first: 10) { edges { node { id price barcode createdAt } } } } } }`, { variables: { product: { title: `${color} Snowboard` } } }, ); const { data } = await createResponse.json(); const product = data!.productCreate!.product!; const variantId = product.variants.edges[0]!.node!.id!; // 2. Update the variant price const updateResponse = await admin.graphql( `#graphql mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id price barcode createdAt } } }`, { variables: { productId: product.id, variants: [{ id: variantId, price: "100.00" }] } }, ); const variantData = await updateResponse.json(); return { product: data!.productCreate!.product, variant: variantData!.data!.productVariantsBulkUpdate!.productVariants, }; // Expected shape: // { // product: { id: "gid://shopify/Product/12345", title: "Red Snowboard", handle: "red-snowboard", status: "ACTIVE", variants: {...} }, // variant: [{ id: "gid://shopify/ProductVariant/67890", price: "100.00", barcode: null, createdAt: "2025-01-01T00:00:00Z" }] // } }; ``` ``` -------------------------------- ### authenticate.webhook(request) Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Verifies the HMAC signature of an incoming Shopify webhook POST request and returns structured data including the shop, topic, payload, and the associated session (if the app is still installed). Each webhook topic should have its own Remix action-only route file under `app/routes/webhooks.*.tsx`. ```APIDOC ## `authenticate.webhook(request)` — Authenticate and parse an incoming webhook Verifies the HMAC signature of a Shopify webhook POST, then returns structured data including `shop`, `topic`, `payload`, and the associated `session` (if the app is still installed). Each webhook topic gets its own Remix action-only route file under `app/routes/webhooks.*.tsx`. ```typescript // app/routes/webhooks.app.uninstalled.tsx import type { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import db from "../db.server"; export const action = async ({ request }: ActionFunctionArgs) => { // Verifies HMAC; throws 401 if invalid const { shop, session, topic } = await authenticate.webhook(request); console.log(`Received ${topic} webhook for ${shop}`); // topic => "APP_UNINSTALLED" // shop => "example.myshopify.com" // session=> Session | undefined (undefined if already deleted) if (session) { // Clean up all sessions for the uninstalled shop await db.session.deleteMany({ where: { shop } }); } return new Response(); // 200 OK required to acknowledge delivery }; ``` ```typescript // app/routes/webhooks.app.scopes_update.tsx — update stored scopes on change import type { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import db from "../db.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { payload, session, topic, shop } = await authenticate.webhook(request); console.log(`Received ${topic} webhook for ${shop}`); const current = payload.current as string[]; // e.g. ["write_products", "read_orders"] if (session) { await db.session.update({ where: { id: session.id }, data: { scope: current.toString() }, // "write_products,read_orders" }); } return new Response(); // 200 OK }; ``` ``` -------------------------------- ### Initialize Shopify App with Remix Template Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use the Shopify CLI to initialize a new app using the specified Remix template. This command sets up the project structure and necessary configurations. ```shell shopify app init --template=https://github.com/Shopify/shopify-app-template-remix ``` -------------------------------- ### shopifyApp() — Initialise the Shopify app instance Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Configures the entire Shopify integration in one call. The returned object exposes every authentication and webhook helper used throughout the app. Exported helpers (`authenticate`, `unauthenticated`, `login`, etc.) are thin re-exports of the configured instance, making them importable anywhere without passing the instance around. ```APIDOC ## shopifyApp() ### Description Initializes the Shopify app instance, configuring authentication, webhooks, and session storage. The returned object provides access to various Shopify API and authentication helpers. ### Method `shopifyApp(config)` ### Parameters #### Configuration Object (`config`) - **apiKey** (string) - Required - Your Shopify API key from the Partner Dashboard. - **apiSecretKey** (string) - Required - Your Shopify API secret key. - **apiVersion** (ApiVersion) - Required - The Shopify Admin API version to use (e.g., `ApiVersion.January25`). - **scopes** (string[]) - Required - An array of OAuth scopes required by your app (e.g., `["write_products", "read_orders"]`). - **appUrl** (string) - Required - The public URL of your app (e.g., a tunnel URL during development). - **authPathPrefix** (string) - Optional - The prefix for OAuth-related routes (defaults to `/auth`). - **sessionStorage** (SessionStorage) - Required - An instance of a session storage adapter (e.g., `PrismaSessionStorage`). - **distribution** (AppDistribution) - Optional - Specifies the app distribution channel (e.g., `AppDistribution.AppStore`). - **future** (object) - Optional - Configuration for future features. - **unstable_newEmbeddedAuthStrategy** (boolean) - Enables the Token Exchange auth strategy. - **expiringOfflineAccessTokens** (boolean) - Enables token rotation for offline access tokens. - **customShopDomains** (string[]) - Optional - A list of custom shop domains to support. ### Exported Helpers - **addDocumentResponseHeaders**: Function to add necessary headers to responses. - **authenticate**: Function to authenticate requests and retrieve session information. - **unauthenticated**: Function for unauthenticated routes. - **login**: Function to initiate the OAuth login flow. - **registerWebhooks**: Function to register webhooks with Shopify. - **sessionStorage**: The configured session storage instance. ### Example ```typescript import { ApiVersion, AppDistribution, shopifyApp, } from "@shopify/shopify-app-remix/server"; import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma"; import prisma from "./db.server"; const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY, apiSecretKey: process.env.SHOPIFY_API_SECRET || "", apiVersion: ApiVersion.January25, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL || "", authPathPrefix: "/auth", sessionStorage: new PrismaSessionStorage(prisma), distribution: AppDistribution.AppStore, future: { unstable_newEmbeddedAuthStrategy: true, expiringOfflineAccessTokens: true, }, ...(process.env.SHOP_CUSTOM_DOMAIN ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] } : {}), }); export default shopify; export const authenticate = shopify.authenticate; export const unauthenticated = shopify.unauthenticated; export const login = shopify.login; export const registerWebhooks = shopify.registerWebhooks; export const sessionStorage = shopify.sessionStorage; ``` ``` -------------------------------- ### Build App with npm Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to build your Remix application when using npm as your package manager. ```shell npm run build ``` -------------------------------- ### Build App with pnpm Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to build your Remix application when using pnpm as your package manager. ```shell pnpm run build ``` -------------------------------- ### Run Shopify CLI with ngrok Tunnel Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to enable local development with streaming responses by specifying a ngrok tunnel URL. Ensure ngrok is set up and running on port 8080. ```bash yarn shopify app dev --tunnel-url=TUNNEL_URL:8080 ``` -------------------------------- ### Shopify App Template Remix CLI Commands Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Provides essential commands for scaffolding, setting up, developing, building, deploying, linting, and generating types for a Shopify app using the Remix template. ```bash # Scaffold a new app from this template npm install -g @shopify/cli@latest shopify app init --template=https://github.com/Shopify/shopify-app-template-remix ``` ```bash # First-time database setup (generates Prisma client + runs migrations) npm run setup # Equivalent to: prisma generate && prisma migrate deploy ``` ```bash # Start local development (authenticates with Partners, opens tunnel, sets env vars) shopify app dev # or npm run dev ``` ```bash # Build for production npm run build ``` ```bash # Deploy config + extensions to Shopify (updates scopes and webhook subscriptions) npm run deploy ``` ```bash # Lint npm run lint ``` ```bash # Regenerate GraphQL TypeScript types from inline queries npm run graphql-codegen ``` ```bash # Docker: setup DB + start production server npm run docker-start ``` -------------------------------- ### Configure Vite for Remix (JavaScript) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Set up the Vite configuration file for a Remix application using JavaScript. This includes server port, HMR settings, and file system access rules. Ensure `tsconfigPaths` is included for path aliases. ```javascript import { vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; if ( process.env.HOST && (!process.env.SHOPIFY_APP_URL || process.env.SHOPIFY_APP_URL === process.env.HOST) ) { process.env.SHOPIFY_APP_URL = process.env.HOST; delete process.env.HOST; } const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost") .hostname; let hmrConfig; if (host === "localhost") { hmrConfig = { protocol: "ws", host: "localhost", port: 64999, clientPort: 64999, }; } else { hmrConfig = { protocol: "wss", host: host, port: parseInt(process.env.FRONTEND_PORT!) || 8002, clientPort: 443, }; } export default defineConfig({ server: { port: Number(process.env.PORT || 3000), hmr: hmrConfig, fs: { // See https://vitejs.dev/config/server-options.html#server-fs-allow for more information allow: ["app", "node_modules"], }, }, plugins: [ remix({ ignoredRouteFiles: ["**/.*"], }), tsconfigPaths(), ], build: { assetsInlineLimit: 0, }, }); ``` -------------------------------- ### Build App with Yarn Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to build your Remix application when using Yarn as your package manager. ```shell yarn build ``` -------------------------------- ### Handle OAuth Redirects with Catch-all Route Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt This minimal loader delegates all `/auth/*` traffic back to `authenticate.admin()`. It internally drives the OAuth handshake and redirects back to the app on success, handling unauthenticated merchants by initiating the OAuth flow. ```typescript // app/routes/auth.$.tsx import type { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { // Handles /auth, /auth/callback, /auth/session-token, etc. // Redirects unauthenticated merchants through the OAuth flow. await authenticate.admin(request); return null; }; ``` -------------------------------- ### Deploy App with npm Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to update your app's scopes with Shopify when they have been changed. ```shell npm run deploy ``` -------------------------------- ### Configure Vite for Remix (TypeScript) Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Set up the Vite configuration file for a Remix application using TypeScript. This includes server port, HMR settings, and file system access rules. Ensure `tsconfigPaths` is included for path aliases. ```typescript import { vitePlugin as remix } from "@remix-run/dev"; import { defineConfig, type UserConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; if ( process.env.HOST && (!process.env.SHOPIFY_APP_URL || process.env.SHOPIFY_APP_URL === process.env.HOST) ) { process.env.SHOPIFY_APP_URL = process.env.HOST; delete process.env.HOST; } const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost") .hostname; let hmrConfig; if (host === "localhost") { hmrConfig = { protocol: "ws", host: "localhost", port: 64999, clientPort: 64999, }; } else { hmrConfig = { protocol: "wss", host: host, port: Number(process.env.FRONTEND_PORT || 8002), clientPort: 443, }; } export default defineConfig({ server: { port: Number(process.env.PORT || 3000), hmr: hmrConfig, fs: { // See https://vitejs.dev/config/server-options.html#server-fs-allow for more information allow: ["app", "node_modules"], }, }, plugins: [ remix({ ignoredRouteFiles: ["**/.*"], }), tsconfigPaths(), ], build: { assetsInlineLimit: 0, }, }) satisfies UserConfig; ``` -------------------------------- ### AppProvider + NavMenu for Embedded App Shell Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Wraps all /app/* pages in the Shopify AppProvider and renders a NavMenu for in-Admin navigation. The boundary helpers ensure that Shopify-specific thrown responses include the required CSP/Frame-Options headers. ```typescript // app/routes/app.tsx import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node"; import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react"; import { boundary } from "@shopify/shopify-app-remix/server"; import { AppProvider } from "@shopify/shopify-app-remix/react"; import { NavMenu } from "@shopify/app-bridge-react"; import polarisStyles from "@shopify/polaris/build/esm/styles.css?url"; import { authenticate } from "../shopify.server"; export const links = () => [{ rel: "stylesheet", href: polarisStyles }]; export const loader = async ({ request }: LoaderFunctionArgs) => { await authenticate.admin(request); return { apiKey: process.env.SHOPIFY_API_KEY || "" }; }; export default function App() { const { apiKey } = useLoaderData(); return ( // isEmbeddedApp=true renders inside Shopify Admin iframe Home Additional page {/* Child routes render here */} ); } // Required: catches thrown Shopify responses and forwards their headers export function ErrorBoundary() { return boundary.error(useRouteError()); } export const headers: HeadersFunction = (headersArgs) => { return boundary.headers(headersArgs); }; ``` -------------------------------- ### OAuth catch-all route (`auth.$`) Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Handles all OAuth-related redirects and traffic for the `/auth/*` routes, delegating to authenticate.admin() for the OAuth flow and token exchange. ```APIDOC ## OAuth catch-all route (`auth.$`) — Handle OAuth redirects A minimal loader that delegates all `/auth/*` traffic back to `authenticate.admin()`, which internally drives the full OAuth / token-exchange handshake and redirects back to the app on success. ### Method GET ### Endpoint /auth/.* ### Description This route acts as a catch-all for any path starting with `/auth/`. It is primarily used to handle OAuth redirects, callbacks, and session token exchanges. The `authenticate.admin` function manages the entire OAuth handshake process, including redirecting unauthenticated merchants to the login flow and handling the subsequent token exchange upon successful authentication. After a successful authentication, the user is redirected back to the application. ### Response #### Success Response (Redirect) Redirects the user to the appropriate location after the OAuth flow is completed or initiates the OAuth flow if the user is unauthenticated. ``` -------------------------------- ### Deploy App with pnpm Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to update your app's scopes with Shopify when they have been changed. ```shell pnpm run deploy ``` -------------------------------- ### Update `shopify.web.toml` for Custom Server Source: https://github.com/shopify/shopify-app-template-remix/wiki/Use-a-custom-server Change the `dev` command in `shopify.web.toml` to `node server.js`. This directs the `shopify app dev` CLI to launch your custom Express server instead of the default Vite development server. ```diff //shopify.web.toml name = "remix" roles = ["frontend", "backend"] webhooks_path = "/webhooks" [commands] - dev = "npx prisma generate && npx prisma migrate deploy && npm exec remix vite:dev" + dev = "npx prisma generate && npx prisma migrate deploy && node server.js" ``` -------------------------------- ### Update dev command in shopify.web.toml Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Change the 'dev' command in shopify.web.toml to use 'remix vite:dev' for running the development server. ```diff [commands] - dev = "npx prisma generate && npx prisma migrate deploy && npm exec remix dev" + dev = "npx prisma generate && npx prisma migrate deploy && npm exec remix vite:dev" - [hmr_server] - http_paths = ["/ping"] ``` -------------------------------- ### Deploy App with Yarn Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Use this command to update your app's scopes with Shopify when they have been changed. ```shell yarn deploy ``` -------------------------------- ### Configure Vite for Vercel Preset Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Modify your vite.config.ts file to include the vercelPreset for hosting on Vercel. Ensure imports are from '@vercel/remix' instead of '@remix-run/node'. ```typescript import { vitePlugin as remix } from "@remix-run/dev"; import { defineConfig, type UserConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; import { vercelPreset } from '@vercel/remix/vite'; installGlobals(); export default defineConfig({ plugins: [ remix({ ignoredRouteFiles: ["**/.*"], presets: [vercelPreset()], }), tsconfigPaths(), ], }); ``` -------------------------------- ### Shopify Webhook Subscriptions Configuration Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Configures app-specific webhook subscriptions in `shopify.app.toml`. These are automatically registered on deploy. Ensure the `api_version` and `topics` match your needs. ```toml # shopify.app.toml client_id = "" # Populated automatically by `shopify app config link` [access_scopes] scopes = "write_products" # Expand as needed, e.g. "write_products,read_orders" [webhooks] api_version = "2024-10" # App uninstall — handled by /app/routes/webhooks.app.uninstalled.tsx [[webhooks.subscriptions]] topics = ["app/uninstalled"] uri = "/webhooks/app/uninstalled" # Scope change — handled by /app/routes/webhooks.app.scopes_update.tsx [[webhooks.subscriptions]] topics = ["app/scopes_update"] uri = "/webhooks/app/scopes_update" # Optional filtered webhook example (uncomment to enable) # [[webhooks.subscriptions]] # topics = ["products/update"] # uri = "/webhooks/products/update" # filter = "variants.price:>=10.00" # Only products with price >= $10 ``` -------------------------------- ### PrismaClient Singleton Initialization Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Initializes a singleton PrismaClient instance to prevent connection pool exhaustion in development environments. For production, ensure the NODE_ENV is set correctly. ```typescript import { PrismaClient } from "@prisma/client"; declare global { var prismaGlobal: PrismaClient; } if (process.env.NODE_ENV !== "production") { if (!global.prismaGlobal) { global.prismaGlobal = new PrismaClient(); } } const prisma = global.prismaGlobal ?? new PrismaClient(); export default prisma; ``` -------------------------------- ### Configure TypeScript for Vite Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Add Vite client and Remix node type references to `env.d.ts` for proper TypeScript intellisense and type checking in a Vite-based Remix project. ```typescript /// /// ``` -------------------------------- ### Configure Express Server for Shopify CLI Source: https://github.com/shopify/shopify-app-template-remix/wiki/Use-a-custom-server Modify the `server.js` file to set the port using the `FRONTEND_PORT` environment variable provided by the Shopify CLI. This ensures the Express server runs on the correct port during development. ```diff // server.js import { createRequestHandler } from "@remix-run/express"; import { installGlobals } from "@remix-run/node"; import compression from "compression"; import express from "express"; import morgan from "morgan"; installGlobals(); const viteDevServer = process.env.NODE_ENV === "production" ? undefined : await import("vite").then((vite) => vite.createServer({ server: { middlewareMode: true }, }) ); const remixHandler = createRequestHandler({ build: viteDevServer ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build") : await import("./build/server/index.js"), }); const app = express(); app.use(compression()); // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable("x-powered-by"); // handle asset requests if (viteDevServer) { app.use(viteDevServer.middlewares); } else { // Vite fingerprints its assets so we can cache forever. app.use( "/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" }) ); } // Everything else (like favicon.ico) is cached for an hour. You may want to be // more aggressive with this caching. app.use(express.static("build/client", { maxAge: "1h" })); app.use(morgan("tiny")); // handle SSR requests app.all("*", remixHandler); + // FRONTEND_PORT is set by the Shopify CLI + const port = process.env.FRONTEND_PORT; app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`) ); ``` -------------------------------- ### GraphQL Codegen Configuration Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Configures `@shopify/api-codegen-preset` to generate TypeScript types from GraphQL queries. Run `npm run graphql-codegen` to output types to `app/types/`. Supports extension schemas. ```typescript // .graphqlrc.ts import fs from "fs"; import { ApiVersion } from "@shopify/shopify-api"; import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset"; import type { IGraphQLConfig } from "graphql-config"; function getConfig() { const config: IGraphQLConfig = { projects: { default: shopifyApiProject({ apiType: ApiType.Admin, // Target: Shopify Admin GraphQL API apiVersion: ApiVersion.July25, // Schema version for type generation documents: [ "./app/**/*.{js,ts,jsx,tsx}", // Scan all app source files "./app/.server/**/*.{js,ts,jsx,tsx}", ], outputDir: "./app/types", // Generated types destination }), }, }; // Auto-discover extension schemas (e.g. Shopify Functions) let extensions: string[] = []; try { extensions = fs.readdirSync("./extensions"); } catch {} for (const entry of extensions) { const schema = `./extensions/${entry}/schema.graphql`; if (!fs.existsSync(schema)) continue; config.projects[entry] = { schema, documents: [`./extensions/${entry}/**/*.graphql`], }; } return config; } export default getConfig(); ``` -------------------------------- ### Update Remix Dependencies and Type Setting Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Ensure Remix dependencies are at least v2.7.0 and set 'type' to 'module' in package.json for compatibility with 'moduleResolution: "Bundler"'. ```diff "dependencies": { - "@remix-run/dev": "^2.0.0", - "@remix-run/node": "^2.0.0", - "@remix-run/react": "^2.0.0", - "@remix-run/serve": "^2.0.0", + "@remix-run/dev": "^2.7.0", + "@remix-run/node": "^2.7.0", + "@remix-run/react": "^2.7.0", + "@remix-run/serve": "^2.7.0", }, "devDependencies": { - "@remix-run/eslint-config": "^2.0.0", + "@remix-run/eslint-config": "^2.7.0", }, ``` -------------------------------- ### Update @shopify/shopify-app-remix Version Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Ensure the @shopify/shopify-app-remix dependency is at least v3.1.0. ```diff "dependencies": { - "@shopify/shopify-app-remix": "^2.4.0", + "@shopify/shopify-app-remix": "^3.1.0", }, ``` -------------------------------- ### Move @remix-run/dev to devDependencies Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Relocate the @remix-run/dev package from dependencies to devDependencies in package.json. ```diff "dependencies": { - "@remix-run/dev": "^2.7.0", }, "devDependencies": { + "@remix-run/dev": "^2.7.0", }, ``` -------------------------------- ### login(request) Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Handles the unauthenticated merchant login flow by redirecting to the OAuth process. It can return a LoginError object for validation failures. ```APIDOC ## `login(request)` — Unauthenticated merchant login flow Used on the public `/auth/login` route (accessible outside the Shopify Admin iframe) to redirect a merchant to the OAuth flow when they enter their shop domain. Returns a `LoginError` object that describes validation failures; use `loginErrorMessage()` to turn those error codes into human-readable strings. ### Method POST ### Endpoint /auth/login ### Parameters #### Request Body - **shop** (string) - Required - The shop domain entered by the merchant. ### Request Example ```json { "shop": "example.myshopify.com" } ``` ### Response #### Success Response (Redirect) Redirects to the Shopify OAuth flow. #### Error Response (LoginError Object) - **shop** (string) - Describes validation failures for the shop domain (e.g., `MissingShop`, `InvalidShop`). ``` -------------------------------- ### Configure Vite HMR for Localhost Source: https://github.com/shopify/shopify-app-template-remix/wiki/Use-a-custom-server Update `vite.config.js` to set the HMR server protocol and host to `ws` and `localhost` respectively. This configuration is specific to using the Chrome web browser. ```javascript //vite.config.js const hmrConfig = { protocol: "ws", host: "localhost", port: 64999, clientPort: 64999, }; ``` -------------------------------- ### Authenticate Incoming Webhook Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Use `authenticate.webhook(request)` in your webhook action to verify the HMAC signature and parse incoming webhook data. It returns the shop, session, topic, and payload. Ensure your webhook routes are action-only. ```typescript import type { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import db from "../db.server"; export const action = async ({ request }: ActionFunctionArgs) => { // Verifies HMAC; throws 401 if invalid const { shop, session, topic } = await authenticate.webhook(request); console.log(`Received ${topic} webhook for ${shop}`); // topic => "APP_UNINSTALLED" // shop => "example.myshopify.com" // session=> Session | undefined (undefined if already deleted) if (session) { // Clean up all sessions for the uninstalled shop await db.session.deleteMany({ where: { shop } }); } return new Response(); // 200 OK required to acknowledge delivery }; ``` ```typescript import type { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import db from "../db.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { payload, session, topic, shop } = await authenticate.webhook(request); console.log(`Received ${topic} webhook for ${shop}`); const current = payload.current as string[]; // e.g. ["write_products", "read_orders"] if (session) { await db.session.update({ where: { id: session.id }, data: { scope: current.toString() }, // "write_products,read_orders" }); } return new Response(); // 200 OK }; ``` -------------------------------- ### Handle Unauthenticated Merchant Login Flow Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Use this on the public `/auth/login` route to redirect merchants to the OAuth flow when they enter their shop domain. It returns a `LoginError` object for validation failures. ```typescript // app/routes/auth.login/route.tsx import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { AppProvider as PolarisAppProvider, Button, Card, FormLayout, Page, TextField } from "@shopify/polaris"; import polarisTranslations from "@shopify/polaris/locales/en.json"; import { login } from "../../shopify.server"; import { loginErrorMessage } from "./error.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { // Attempt login on page load (handles redirect-back-here scenarios) const errors = loginErrorMessage(await login(request)); return { errors, polarisTranslations }; }; export const action = async ({ request }: ActionFunctionArgs) => { // Attempt login on form submission const errors = loginErrorMessage(await login(request)); return { errors }; }; export default function Auth() { const loaderData = useLoaderData(); const actionData = useActionData(); const { errors } = actionData || loaderData; return (
); } ``` -------------------------------- ### Update Node Engine Version Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Change the minimum required Node.js version in package.json from '>=18.0.0' to '>=20.0.0'. ```diff "engines": { - "node": ">=18.0.0", + "node": ">=20.0.0", }, ``` -------------------------------- ### Update shopify-app-remix Dependency Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Ensure your `@shopify/shopify-app-remix` dependency is updated to at least version 3.1.0 in your `package.json` file. ```diff "dependencies": { - "@shopify/shopify-app-remix": "^2.4.0", + "@shopify/shopify-app-remix": "^3.1.0", }, ``` -------------------------------- ### Remove Shopify CLI script Source: https://github.com/shopify/shopify-app-template-remix/wiki/Migrate-your-Remix-app-to-Vite Remove the 'shopify' script alias from package.json if it exists. ```diff "scripts": { - "shopify": "shopify", }, ``` -------------------------------- ### addDocumentResponseHeaders for Shopify HTTP Headers Source: https://context7.com/shopify/shopify-app-template-remix/llms.txt Adds Shopify-required HTTP headers (CSP, X-Frame-Options, etc.) on every server-rendered HTML document response. This is crucial for embedded apps to function correctly within the Shopify Admin iframe. ```typescript // app/entry.server.tsx import { PassThrough } from "stream"; import { renderToPipeableStream } from "react-dom/server"; import { RemixServer } from "@remix-run/react"; import { createReadableStreamFromReadable, type EntryContext } from "@remix-run/node"; import { isbot } from "isbot"; import { addDocumentResponseHeaders } from "./shopify.server"; export const streamTimeout = 5000; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { // Inject required Shopify headers (CSP, frame-ancestors, etc.) addDocumentResponseHeaders(request, responseHeaders); // Use onAllReady for bots (full render), onShellReady for browsers (stream) const callbackName = isbot(request.headers.get("user-agent") ?? "") ? "onAllReady" : "onShellReady"; return new Promise((resolve, reject) => { const { pipe, abort } = renderToPipeableStream( , { [callbackName]: () => { const body = new PassThrough(); responseHeaders.set("Content-Type", "text/html"); resolve( new Response(createReadableStreamFromReadable(body), { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError: reject, onError(error) { responseStatusCode = 500; console.error(error); }, } ); setTimeout(abort, streamTimeout + 1000); // 6 s hard timeout }); } ``` -------------------------------- ### Prisma Schema for MongoDB Session Source: https://github.com/shopify/shopify-app-template-remix/blob/main/README.md Define the Session model in your Prisma schema to correctly map MongoDB's `_id` field to Prisma's `id` field. This is necessary for MongoDB compatibility with Prisma. ```prisma model Session { session_id String @id @default(auto()) @map("_id") @db.ObjectId id String @unique ... ```