@shopify/shopify-app-remix makes it easy to build Shopify apps using the Remix framework. It wraps @shopify/shopify-api with Remix-specific utilities for authentication, session management, and API access.
Installation
npm install @shopify/shopify-app-remix @shopify/shopify-app-session-storage-prisma
Requires Node.js >= 20.10.0 and Remix >= 2.0.0
Quick Start
Create app/shopify.server.ts:
import { shopifyApp } from "@shopify/shopify-app-remix/server" ;
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma" ;
import { restResources } from "@shopify/shopify-api/rest/admin/2024-01" ;
import prisma from "~/db.server" ;
const shopify = shopifyApp ({
apiKey: process . env . SHOPIFY_API_KEY ! ,
apiSecretKey: process . env . SHOPIFY_API_SECRET ! ,
scopes: process . env . SCOPES ?. split ( "," ) ! ,
appUrl: process . env . SHOPIFY_APP_URL ! ,
apiVersion: "2024-01" ,
sessionStorage: new PrismaSessionStorage ( prisma ),
restResources ,
// Optional: configure webhooks
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: "http" ,
callbackUrl: "/webhooks" ,
},
},
// Optional: lifecycle hooks
hooks: {
afterAuth : async ({ session }) => {
// Register webhooks after authentication
await shopify . registerWebhooks ({ session });
},
},
});
export default shopify ;
export const authenticate = shopify . authenticate ;
2. Add Authentication Routes
Remix adapter handles authentication automatically. Create app/routes/auth.$.tsx:
import { LoaderFunctionArgs } from "@remix-run/node" ;
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
await authenticate . admin ( request );
return null ;
};
3. Authenticate Admin Requests
In your route loaders and actions:
import { LoaderFunctionArgs , 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 );
const response = await admin . graphql (
`#graphql
query {
products(first: 10) {
edges {
node {
id
title
handle
}
}
}
}
`
);
const data = await response . json ();
return json ({ products: data . data . products . edges });
};
export default function ProductsPage () {
const { products } = useLoaderData < typeof loader >();
return (
< div >
{ products . map (({ node }) => (
< div key = {node. id } > {node. title } </ div >
))}
</ div >
);
}
Configuration
shopifyApp Options
Your app’s API key from the Partner Dashboard. Typically: process.env.SHOPIFY_API_KEY
Your app’s API secret from the Partner Dashboard. Typically: process.env.SHOPIFY_API_SECRET
OAuth scopes your app needs. Example: ["read_products", "write_orders"]
Your app’s URL (including protocol). Example: "https://myapp.example.com" or process.env.SHOPIFY_APP_URL
Shopify API version to use. Example: "2024-01"
Whether the app is embedded in Shopify Admin.
Whether to use online access tokens instead of offline. When true, both online and offline tokens are saved for background jobs.
distribution
AppDistribution
default: "AppDistribution.AppStore"
App distribution type:
AppDistribution.AppStore: Public app
AppDistribution.SingleMerchant: Private app
AppDistribution.ShopifyAdmin: Custom app in Shopify Admin
Shop-specific webhook configuration. See Webhooks section.
Billing configuration for subscriptions. See Billing section.
Authentication
Admin Authentication
Authenticate admin requests in loaders and actions:
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin , session , billing , cors } = await authenticate . admin ( request );
// admin: GraphQL/REST client
// session: Current session
// billing: Billing utilities
// cors: CORS helper
return json ({ shop: session . shop });
};
API client for making Admin API requests. Methods:
admin.graphql(): Make GraphQL requests
admin.rest: Access REST API
Current authenticated session. Properties:
session.shop: Shop domain
session.accessToken: Access token
session.scope: Granted scopes
Billing utilities for the current shop. Methods:
billing.require(): Require active billing
billing.request(): Request new subscription
billing.check(): Check billing status
Helper to add CORS headers to responses.
GraphQL Requests
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin } = await authenticate . admin ( request );
const response = await admin . graphql (
`#graphql
query getProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
}
}
}
}
` ,
{
variables: { first: 10 },
}
);
const data = await response . json ();
return json ( data . data );
};
REST API Requests
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin } = await authenticate . admin ( request );
// Using REST client
const response = await admin . rest . get ({ path: "products" });
const products = response . body . products ;
// Using REST resources (type-safe)
const productsTyped = await admin . rest . resources . Product . all ({
session: admin . session ,
limit: 10 ,
});
return json ({ products });
};
Webhook Authentication
Handle incoming webhooks:
// app/routes/webhooks.tsx
import { ActionFunctionArgs } from "@remix-run/node" ;
import { authenticate } from "~/shopify.server" ;
export const action = async ({ request } : ActionFunctionArgs ) => {
const { topic , admin , payload , session } = await authenticate . webhook ( request );
// Session may be undefined if app is uninstalled
if ( ! session ) {
throw new Response ();
}
switch ( topic ) {
case "PRODUCTS_UPDATE" :
await admin . graphql (
`#graphql
mutation setMetafield($productId: ID!, $time: String!) {
metafieldsSet(metafields: {
ownerId: $productId
namespace: "my-app",
key: "webhook_received_at",
value: $time,
type: "string",
}) {
metafields {
key
value
}
}
}
` ,
{
variables: {
productId: payload . admin_graphql_api_id ,
time: new Date (). toISOString (),
},
}
);
break ;
}
return new Response ();
};
Public Authentication
Handle public requests (App Proxy, Checkout UI Extensions):
// App Proxy
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { liquid , storefront } = await authenticate . public . appProxy ( request );
// Make Storefront API requests
const response = await storefront . graphql (
`query { shop { name } }`
);
// Return Liquid template
return liquid ( "<h1>{{ shop.name }}</h1>" );
};
Webhooks
Registering Webhooks
In Configuration
Manual Registration
import { DeliveryMethod } from "@shopify/shopify-app-remix/server" ;
const shopify = shopifyApp ({
// ... other config
webhooks: {
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod . Http ,
callbackUrl: "/webhooks" ,
},
ORDERS_PAID: {
deliveryMethod: DeliveryMethod . Http ,
callbackUrl: "/webhooks" ,
},
},
hooks: {
afterAuth : async ({ session }) => {
// Register webhooks after OAuth
await shopify . registerWebhooks ({ session });
},
},
});
import shopify from "~/shopify.server" ;
// In a loader or background job
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { session } = await authenticate . admin ( request );
// Register all configured webhooks
const response = await shopify . registerWebhooks ({ session });
return json ( response );
};
Billing
Handle app billing and subscriptions:
import { shopifyApp , BillingInterval } from "@shopify/shopify-app-remix/server" ;
const shopify = shopifyApp ({
// ... other config
billing: {
"Premium Plan" : {
amount: 10.0 ,
currencyCode: "USD" ,
interval: BillingInterval . Every30Days ,
trialDays: 7 ,
},
},
});
Require Active Billing
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { billing } = await authenticate . admin ( request );
// Require active billing or redirect to payment
await billing . require ({
plans: [ "Premium Plan" ],
onFailure : async () => billing . request ({ plan: "Premium Plan" }),
});
// User has active subscription
return json ({ success: true });
};
Session Storage
Choose a session storage adapter:
Prisma
PostgreSQL
MongoDB
npm install @shopify/shopify-app-session-storage-prisma
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma" ;
import prisma from "~/db.server" ;
const shopify = shopifyApp ({
sessionStorage: new PrismaSessionStorage ( prisma ),
});
npm install @shopify/shopify-app-session-storage-postgresql
import { PostgreSQLSessionStorage } from "@shopify/shopify-app-session-storage-postgresql" ;
const shopify = shopifyApp ({
sessionStorage: new PostgreSQLSessionStorage (
"postgresql://user:pass@localhost/db"
),
});
npm install @shopify/shopify-app-session-storage-mongodb
import { MongoDBSessionStorage } from "@shopify/shopify-app-session-storage-mongodb" ;
const shopify = shopifyApp ({
sessionStorage: new MongoDBSessionStorage (
"mongodb://localhost:27017" ,
"sessions"
),
});
Lifecycle Hooks
const shopify = shopifyApp ({
// ... other config
hooks: {
afterAuth : async ({ session , admin }) => {
// Called after successful OAuth
console . log ( `Shop ${ session . shop } installed the app` );
// Register webhooks
await shopify . registerWebhooks ({ session });
// Create initial data
await admin . graphql (
`mutation { /* setup query */ }`
);
},
},
});
Error Handling
Handle authentication errors:
import { boundary } from "@shopify/shopify-app-remix/server" ;
// Error boundary for authentication errors
export const ErrorBoundary = boundary . error ;
Always use authenticate.admin() in routes that require authentication to ensure proper session validation.
Next Steps
Core API Learn about @shopify/shopify-api
Session Storage Explore session storage options
Shopify CLI Use Shopify CLI for development