Prisma Offline Session Row Disappears After App Is Left Idle - Investigating Token Refresh

Hi everyone,

We are using:

  • Shopify React Router

  • @shopify/shopify-app-session-storage-prisma

  • Prisma

  • Expiring offline access tokens

  • Shopify Flow/background requests

Our Shopify configuration has:

sessionStorage: new PrismaSessionStorage(prisma),

future: {
  expiringOfflineAccessTokens: true,
},

How we handle the session

For Flow/background requests, we first check for the offline session in Prisma:

export async function getStoreAccessTokenFromPrisma(
  shop: string,
): Promise<string | null> {
  const shopSession = await prisma.session.findFirst({
    where: { shop, isOnline: false },
  });

  const accessToken = shopSession?.accessToken;

  return typeof accessToken === "string" && accessToken.trim()
    ? accessToken
    : null;
}

Then we use unauthenticated.admin(shop) when the Prisma session exists:

export async function getAdminForShop(
  shop: string,
): Promise<GetAdminForShopResult | null> {
  const prismaAccessToken = await getStoreAccessTokenFromPrisma(shop);

  if (prismaAccessToken) {
    return unauthenticated.admin(shop);
  }
}

So the expected flow is:

Shopify Flow request
↓
Check Prisma offline session
↓
Session exists
↓
unauthenticated.admin(shop)
↓
Shopify handles token refresh

We also added temporary logging to check the session state:

await logErrorToFirebase({
  error: new Error("SESSION_DEBUG_SNAPSHOT"),
  module: "SESSION_DEBUG_SNAPSHOT",
  context: {
    shop,
    found: !!shopSession,
    expires: shopSession?.expires ?? null,
    hasRefreshToken: !!shopSession?.refreshToken,
    refreshTokenExpires: shopSession?.refreshTokenExpires ?? null,
    now: new Date().toISOString(),
  },
});

What we observed

For our test store, on September 18:

found: true
hasRefreshToken: true
expires: September 18, 2026 4:26:47 PM IST
refreshTokenExpires: December 17, 2026 3:26:47 PM IST

So the Prisma session definitely existed and had a refresh token.

On September 19, the same lookup returned:

found: false
hasRefreshToken: false
expires: null
refreshTokenExpires: null

So the entire Prisma session row was missing.

There is no prisma.session.delete() or prisma.session.deleteMany() in this code path.

What happens after opening the app

After we observed that the Prisma session was missing, we opened the Shopify app again.

Our afterAuth hook runs after authentication:

sessionStorage: new PrismaSessionStorage(prisma),

future: {
  expiringOfflineAccessTokens: true,
},

hooks: {
  afterAuth: async ({ session }) => {
    await saveStoreData({
      shop: session.shop,
      accessToken: session.accessToken!,
    });

    const { admin } = await shopify.unauthenticated.admin(session.shop);

    // other afterAuth logic...
  },
},

We added temporary logging inside afterAuth and got:

shop: xxxxx.myshopify.com
isOnline: false
hasAccessToken: true
hasRefreshToken: true
expires: 2026-09-19T17:36:17.318Z
refreshTokenExpires: 2026-12-18T16:36:18.318Z

This suggests the lifecycle is:

September 18
Prisma offline session exists
refresh token exists
        ↓
September 19
Prisma offline session is missing
        ↓
Merchant opens the app
        ↓
afterAuth runs
        ↓
Offline session exists again
with access token + refresh token

We are trying to understand what could cause the original Prisma session row to disappear before the app was opened again.

Multiple requests

Our app can receive multiple Flow/background requests for the same shop close together.

For example:

08:56:16 → POST /api/flow/action/assignTier
08:56:22 → hasPrismaAccessToken: true
08:56:29 → POST /api/flow/action/assignTier
08:56:30 → hasPrismaAccessToken: true

These requests can both reach:

unauthenticated.admin(shop);

We are therefore investigating whether concurrent refreshes could be related to the session disappearing.

Questions

Could someone help us understand:

  1. Does unauthenticated.admin(shop) already handle concurrent refreshes for the same shop?

  2. What happens if two requests try to refresh the same offline token at the same time?

  3. Can a 401 during token refresh cause @shopify/shopify-app-session-storage-prisma to delete the Prisma session?

  4. Is there another reason an existing Prisma offline session row could disappear?

Our main concern is that the session existed with a refresh token on September 18, the entire Prisma row was missing on September 19, and opening the app caused an offline session with a new access & refresh-token pair to be available again.

Any guidance would be appreciated. Thank you!

Shopify’s own guidance on this is to serialize refresh operations per shop rather than let them race, so the concurrent Flow requests hitting unauthenticated.admin(shop) close together are the likely trigger. I haven’t seen documented behavior where a 401 mid refresh deletes the row, it’s more likely the two refreshes overwrite each other and the write that lands last is briefly inconsistent with what the other request read.

We haven’t specifically tested concurrent refreshes yet. We mentioned it only as a possible scenario because multiple Flow requests can occur close together.

Our main concern is that the Prisma offline session existed with a refresh token on September 18, but on September 19 the entire session row was missing.

So we’re mainly trying to understand what could cause the existing Prisma session to disappear even though we don’t explicitly delete it.

One more thing worth checking: with expiring offline tokens, a failed refresh (401 or invalid_grant) can make the library delete the stored session instead of just failing the request, which would explain the row vanishing and then reappearing once afterAuth runs again. I’d check your logs for a refresh failure right before the row went missing on the 19th.