Appearance
Auth & sessions
Auth is the topic that confuses everyone at first. That's because there are three different tokens. Learn which is which, and the rest follows.
The three tokens
| Token | Who issues it | Lives | Used for |
|---|---|---|---|
| Session token (JWT) | App Bridge, in the browser | ~1 minute | Proves to your server which shop and user is making this request |
| Access token | Shopify, via token exchange or OAuth | Offline: 1 h (expiring) + refresh token. Online: tied to the user's session | Lets your server call the Admin API |
| Refresh token | Shopify, with an expiring offline token | 90 days, rotates on every refresh | Gets a new offline access token without the merchant |
Modern flow: managed install + token exchange
The old OAuth redirect dance is no longer the default for embedded apps:
- Shopify handles install. Scopes come from
shopify.app.toml. That is managed installation. - Your app gets session tokens from App Bridge on every request.
- Your server exchanges the session token for an access token in a single server-to-server call. That is token exchange.
With the React Router template, all of this is one line: await authenticate.admin(request).
Online vs offline access tokens
| Offline | Online | |
|---|---|---|
| Tied to | The shop | One staff member's login |
| Use for | Background jobs, webhooks, cron | Actions that must respect the user's permissions |
| Default in template | ✅ | Opt-in (useOnlineTokens: true) |
Most apps only need offline.
⚠️ Expiring offline tokens (new rule)
Required for public apps
- Public apps created on or after 2026-04-01 must use expiring offline access tokens.
- Public apps created before that must migrate by 2027-01-01.
- The access token lasts 1 hour. The refresh token lasts 90 days and rotates: each refresh returns a new one, and you must store it.
- You request the new type with
expiring=1on the token exchange call. - Custom and merchant-created apps are exempt.
If you use the official libraries, refreshing is handled for you. If you write your own background workers (like a FlowFlex Moleculer service calling Shopify), you must refresh before calling the API and persist the new refresh token.
Source: Expiring offline access tokens
Calling Shopify from outside the app (e.g. a worker)
ts
import shopify from "~/shopify.server";
// in a job: load the stored offline session for the shop
const { admin } = await shopify.unauthenticated.admin("my-shop.myshopify.com");
await admin.graphql(`{ shop { name } }`);unauthenticated.admin doesn't check a request. It loads the stored offline session, so only call it from trusted server code.
Classic OAuth (authorization code grant)
You still need this for non-embedded apps and for some external integrations. Shopify redirects the merchant to /admin/oauth/authorize, then back to your callback with a code. You exchange the code for a token and verify the hmac and state on the callback. The React Router package doesn't support non-embedded apps, so use the plain @shopify/shopify-api library for that.
Check yourself
- Why can't you use a session token to call the Admin API directly?
- Your cron job gets
401at 3 a.m. What likely happened with expiring tokens? - When would you need an online token?