Hi everyone,
I’m building a Shopify app using React Router v7 and Polaris (both React and Web Components), and I’m running into a strange 401 Unauthorized issue that only happens in production.
What’s happening
Right after installation, the app works fine. But after a few hours, the embedded app starts showing 401 Unauthorized instead of loading. If I delete the session from the database, it never gets recreated and the 401 continues.
What makes this tricky is that I can’t reproduce this locally at all .
Setup overview
- React Router v7
- Polaris React Components + Polaris Web Components
- PostgreSQL with Prisma
- Session storage:
@shopify/shopify-app-session-storage-prisma - Custom distribution (not App Store)
- API version: January26 (also tested October25)
- expiringOfflineAccessTokens: true enabled
Shopify config
Our shopify.server.ts is basically identical to the official template:
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || '',
apiVersion: ApiVersion.January26,
scopes: process.env.SCOPES?.split(','),
appUrl: process.env.SHOPIFY_APP_URL || '',
authPathPrefix: '/auth',
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
future: {
expiringOfflineAccessTokens: true,
},
});
And the auth.$.tsx route is also straight from the template:
export const loader = async ({ request }: LoaderFunctionArgs) => {
await authenticate.admin(request);
return null;
};
What I’m seeing in production logs
I’ve implemented a custom wrapper which sort of implements the PrismaSessionStorage in a dirty way to get some insights and to be able to add some logs.
This is the consistent flow:
- First request to / includes an id_token → redirect to /auth/session-token (302)
- /auth/session-token is hit and returns HTML (200)
- No session storage logs at all — storeSession() is never called
- Second request to / (no id_token) → 401 Unauthorized
- Database shows zero sessions for that shop
Relevant log output:
// Log from: /app/routes/auth.$.tsx - loader function
// This is called when user accesses /auth/session-token endpoint
[AUTH-DEBUG] OAuth flow starting {
pathname: '/auth/session-token', // The OAuth session-token endpoint
shop: 'xyz.myshopify.com',
hasCode: false, // No OAuth authorization code - OAuth callback hasn't completed yet
hasHmac: true, // HMAC signature is present (request is valid)
hasIdToken: false // No ID token - user session not established
}
// Log from: /app/routes/auth.$.tsx - catch block
// authenticate.admin() threw a Error response with status 200
// The Response contains HTML that should complete OAuth via JavaScript i guess
[AUTH-DEBUG] OAuth flow redirect (expected) {
status: 200, // error.status - This is the session-token HTML page i assume, not an error
}
// Log from: /app/routes/auth.$.tsx - after catching the Response
// We check the database to see if any sessions were created during this OAuth flow
[AUTH-DEBUG] Existing sessions in DB after session-token {
shop: 'c11a74-82.myshopify.com',
count: 0, // PROBLEM: No sessions exist - session was never created!
sessions: [] // This confirms that storeSession() was never called
}
Local vs production behavior
- Locally: Sessions are created, tokens refresh correctly, and deleted/expired sessions get recreated.
- Production: Sessions are never created during OAuth. Only the very first installation ever produced a session — after that, nothing.
Things I’ve already tried
- Simplified auth.$.tsx to exactly match the official example
- Double-checked all environment variables (SHOPIFY_API_KEY, SHOPIFY_API_SECRET, SHOPIFY_APP_URL, SCOPES)
- Updated Prisma schema for refreshToken and refreshTokenExpires
- Tested both October25 and January26 API versions
- Verified DB connectivity (works fine)
- Confirmed SCOPES is correctly set in Docker Compose
Questions
- Why would /auth/session-token return the HTML page (200) but never create a session?
- Is the session-token page supposed to trigger another request to complete OAuth? I don’t see any follow-up request in the logs.
- Are there known pitfalls that could affect session creation?
If anyone has run into something similar or has ideas on what to check next, I’d really appreciate it. Thanks in advance!