Key takeaways
- With Shopify managed installation, Shopify installs the app with the scopes in its configuration, then opens it in the admin. There's no authorization redirect and no second approval screen.
- The first embedded load carries a session token, which Inference Bureau trades for the store's offline token (token exchange). That records the install and runs the app's install action.
- Every install gets an expiring offline token. Inference Bureau seals it, renews it before it runs out, and never hands it to the app's code.
- Managed installation is the default for Shopify apps on Apps. An app that needs the redirect keeps it with one line in settings.ib.ts.
For years, installing a Shopify app meant building an OAuth flow. Every app wrote the same handful of routes, and every app could get them subtly wrong. Shopify's managed installation and token exchange remove most of that work. Since October 19, they're how Shopify apps on Apps install by default.
Here's what changed, what your app no longer has to build, and how Inference Bureau looks after the store's token once it has one.
The redirect flow, and what it asked of every app
The legacy install is OAuth's authorization-code flow:
- The merchant clicks install, and Shopify loads the app's install route with the shop's domain.
- The app redirects to
https://<shop>/admin/oauth/authorizewith its client id, the scopes it wants, a redirect URL and a signedstatevalue. - Shopify shows the merchant a grant screen. On approval, it redirects back to the app's callback with a
code. - The callback checks Shopify's HMAC on the query, checks that
stateis the one it issued, and posts the code to/admin/oauth/access_tokenfor a token. - The app stores the token, then sends the merchant back into the admin, where the app finally loads.
That's an install route, a callback, state signing, HMAC checks and token storage, all before the app shows its first page. Changing scopes later meant sending the merchant through the redirect again.
Managed installation and token exchange
With managed installation, the app declares its scopes in its configuration, and Shopify handles the install itself. The merchant sees Shopify's own grant screen once, and then the app opens embedded in the admin.
That first load carries a session token: a short-lived JWT that Shopify signs with the app's client secret, in the URL as id_token. The app trades it for the store's offline token with a token exchange request to the same /admin/oauth/access_token endpoint. Here's the body Inference Bureau sends (from exchangeSessionToken in packages/apps/src/shopify.ts):
{
"client_id": "<client id>",
"client_secret": "<client secret>",
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "<the session token>",
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"requested_token_type": "urn:shopify:params:oauth:token-type:offline-access-token",
"expiring": "1"
}
The answer holds the access token, the scopes the store granted, and, because of expiring, an expiry and a refresh token.
What your app no longer builds
On Apps, your Shopify app's code doesn't contain any of this. Inference Bureau releases the app's configuration to Shopify, with its scopes and any optional scopes. It leaves out use_legacy_install_flow, which keeps Shopify on the managed path. When a store opens the app for the first time:
- The edge router sees the load. An embedded page request with
id_tokenandshopin its URL goes to the API before the page does. The router waits for the answer, so the page's own calls find the store installed. - The token is checked as Shopify documents it. The signature is checked against the client secret, then
expandnbfwith ten seconds of leeway. The audience must be this environment's client id, and the issuer and destination must name the same store. - The plan is checked. On the Free plan, the team's production apps can be installed on five stores in all, so a sixth is refused here.
- The token is exchanged and the install recorded. The store's tokens are sealed and saved, and an
installevent goes to Logs withvia: "token exchange". - What the callback would have done runs. The
shopifyShopmodel'sinstallaction runs with the store's granted scopes, every synced Shopify model is backfilled for the store, and the app proxy's path is written to the store for the app's blocks.
If the router's check doesn't get through, the app's own session check does the same thing as a fallback. Every admin route already calls verifyShopifySession(request), so the first route the page calls records the install.
Scope changes without a reinstall
Under the redirect flow, adding a scope meant sending each store back through OAuth. With managed installation, the app releases its new scopes, and Shopify asks the merchant to approve them the next time they open the app. That load's session token is exchanged again. The new grant is saved, a scopes update event lists the scopes that were added, and the reinstall action runs. A store that's still missing scopes is retried at most once a minute.
A scope that a feature needs only once a merchant uses it can be optional instead:
// settings.ib.ts
// shopify: { optionalScopes: ["read_orders"] },
// web/app/api/orders/route.ts
import { scopesRequired, verifyShopifySession } from "@inferencebureau/apps-sdk";
export async function GET(request: Request) {
const session = await verifyShopifySession(request);
if (!session) return Response.json({ error: "Open this page from the Shopify admin." }, { status: 401 });
if (!session.scopes.includes("read_orders")) return scopesRequired(["read_orders"]);
// …
}
scopesRequired answers 403 with the missing scopes. On the page, sessionFetch turns that into Shopify's approval prompt and sends the request again once the scopes are granted.
Expiring tokens, renewed for you
Every install asks for an expiring offline token, through the OAuth callback and token exchange alike. The access token lasts an hour, and the refresh token lasts 90 days. Inference Bureau keeps both sealed per store, and the app's code never sees either one. Actions get a shopify client that's already authorized for the store.
Renewal happens in adminToken (packages/apps/src/shopify-store.ts):
- Before a call. When the access token runs out within five minutes, it's refreshed first.
- One refresh per store at a time. Shopify can leave one of two concurrent refreshes holding a replaced token, so each refresh runs under a database lock on the install. A call that waited on the lock reads the token the first one stored.
- Failures keep the old pair. If a refresh fails on the network, a 5xx or a 429, Shopify still honours the old tokens, so the call goes ahead while the access token lasts.
- Refusals are final. If Shopify answers 401 to a refresh, the refresh token is cleared, and the store has to install again from Settings → Installs → Update scopes.
- Idle stores aren't forgotten. Each refresh starts a new 90-day refresh token, so an hourly job renews any store whose refresh token runs out within two weeks, even if the app hasn't called it.
Stores installed before expiring tokens still hold a non-expiring token. For a public app, the deploy plan's Review readiness counts them, and they move over with a reinstall.
Keeping the redirect
Some apps need the authorization redirect, for example to run their own step before the install finishes. One line keeps it:
// settings.ib.ts
export const settings: AppSettings = {
type: "ib/settings/v1",
platform: "shopify",
shopify: { install: "oauth" },
};
Installs then go through Inference Bureau's OAuth callback, which checks the signed state and Shopify's HMAC and exchanges the code for an expiring token. Scope changes need Update scopes again. A public app's Review readiness warns about it, since Built for Shopify looks for managed installation.
Frequently asked questions
Do I need to change an existing Shopify app on Apps?
No. Managed installation is the default unless settings.ib.ts says install: "oauth". Stores installed earlier keep their install, and the next admin load picks up scopes approved since.
Does the app's code ever see the store's Admin token?
No. Inference Bureau keeps it sealed and renews it. Actions use the shopify client they're given, and admin routes use verifyShopifySession, which returns the shop, user and granted scopes.
What happens if a store's refresh token stops working?
Shopify's 401 clears it, calls to that store fail with a message naming Settings → Installs, and Update scopes installs the app again with a new token.
Does token exchange work in the dashboard's preview?
No exchange happens there. The preview signs its own session for stores the environment is already installed on, and refuses others.