I'm building a Shopify app using the official @shopify/shopify-app-remix framework with PrismaSessionStorage.
In my App Proxy route, I try to send an authenticated Admin GraphQL request using an offline session retrieved via api.session.getOfflineId(shop). But no matter what I try, I always get this error:
GraphQL Error: HttpResponseError: Received an error response (401 Unauthorized) from Shopify: "GraphQL Client: Unauthorized"
This happens even though:
- The HMAC signature from the App Proxy is verified and valid.
- The session is successfully retrieved from Prisma using
getOfflineId(). session.accessTokenis present and not expired.- The app has been reinstalled after updating scopes.
- The scopes in
shopify.app.tomland.envare correct. - Even the simplest GraphQL query fails.
shopify.app.toml
client_id = "..."
name = "Cart Reminder Workflow Trigger"
application_url = "https://my-app-url.trycloudflare.com"
embedded = true
[access_scopes]
scopes = "write_app_proxy,read_customers,write_customers"
[app_proxy]
url = "https://my-app-url.trycloudflare.com/api"
prefix = "apps"
subpath = "cart-reminder"
[auth]
redirect_urls = [
"https://my-app-url.trycloudflare.com/auth/callback",
"https://my-app-url.trycloudflare.com/api/auth/callback"
]
shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
} from "@shopify/shopify-app-remix/server";
import { shopifyApi } from "@shopify/shopify-api";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const sessionStorage = new PrismaSessionStorage(prisma);
const appUrl = process.env.SHOPIFY_APP_URL || "";
const url = new URL(appUrl);
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
apiVersion: ApiVersion.January25,
scopes: process.env.SCOPES!.split(","),
appUrl,
authPathPrefix: "/auth",
sessionStorage,
distribution: AppDistribution.AppStore,
future: {
unstable_newEmbeddedAuthStrategy: true,
removeRest: true,
},
});
export const api = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
apiVersion: ApiVersion.January25,
scopes: process.env.SCOPES!.split(","),
hostName: url.hostname,
hostScheme: url.protocol.replace(":", "") as "http" | "https",
isEmbeddedApp: true,
});
export default shopify;
export const authenticate = shopify.authenticate;
export const sessionStorageInstance = sessionStorage;
App Proxy Route (app/routes/api.backend-collector.tsx)
const sessionId = api.session.getOfflineId(shop);
const session = await shopify.sessionStorage.loadSession(sessionId);
if (!session || !session.accessToken) {
return json({ error: "Unauthorized" }, { status: 401 });
}
const admin = new api.clients.Graphql({ session });
const result = await admin.request(`{ shop { name } }`);
Debugging Summary:
- The session is loaded via
getOfflineId(), and the token is present. - The access scopes are included in
shopify.app.tomland match.env. - The app was reinstalled after changing scopes.
- HMAC signature from App Proxy is valid and verified.
What could cause my 401 from the GraphQL Admin API when using an offline session?