Shopify App OAuth 

Shopify apps that will be installed on multiple shops must implement Shopify's OAuth process to get installed. Gadget provides a well-tested implementation of Shopify's OAuth out of the box for each Gadget app.

Gadget's Shopify OAuth implementation 

Gadget implements Shopify OAuth on behalf of each Gadget application. To complete the OAuth process, your application needs to redirect merchant visits into Gadget's OAuth flow, and then handle the final request once the flow is complete.

Here's the OAuth process in detail:

  1. The OAuth process is initiated by the merchant when they install the app from the Shopify App Store or a given install link.
  2. The merchant is redirected to the App URL as set in the Shopify Partners dashboard for your application. If the merchant hasn't installed the app before, the app is responsible for initiating the OAuth process to install the application. Gadget provides a default App URL of /api/shopify/install-or-render which will do this automatically.
  3. The app initiates installation by redirecting the merchant to Gadget's OAuth kickoff endpoint. Gadget's generated frontend UI detects uninstalled merchants and does this automatically.
  4. Gadget's OAuth implementation redirects the merchant to Shopify's OAuth consent screen.
  5. The merchant views the app's requested permissions.
  6. The merchant confirms consent for these scopes, and clicks the Install button.
  7. Shopify marks the app as installed, and redirects the merchant to the OAuth Callback URL as set in the Shopify Partners dashboard for your application. If configured correctly, this will send the merchant back to Gadget's platform OAuth implementation at /api/connections/shopify/auth/callback.
  8. Gadget's platform OAuth implementation runs the install action on the shopifyShop model, recording the given access_token from Shopify and running any custom action code you've added to your app.

Gadget only supports offline access tokens.

  1. Gadget's platform OAuth implementation redirects the merchant to your app embedded within the Shopify Admin.
  2. The merchant views the app's UI in the embedded iframe, rendering the frontend React application from your app's frontend folder.

Configuration values 

Gadget provides a value for both the App URL and the OAuth Callback URL for saving in the Partners dashboard. Gadget recommends using the defaults provided when creating your application:

Configuration ValueDescription
App URLThe URL that Shopify will send each merchant to, both for installation and for rendering the app each time a merchant uses it.

Default value: <your-app-domain>/api/shopify/install-or-render
OAuth Callback URLThe URL that Shopify will redirect merchants who have granted permission to your app. Part of the OAuth process.

Default value: <your-app-domain>/api/connections/shopify/auth/callback

Find the values for your application in the Shopify section of your application's Shopify Connection page in the Gadget editor.

Connections page showing the redirect URLs for a specific application

In the past, Gadget recommended that Shopify applications utilize the /shopify/install URL as the configuration value for their App URL setting within Shopify. This URL served as the entry point where Shopify directed both newly uninstalled merchants looking to install an app and existing merchants seeking to use the application. The performance of this particular route was critical, as it represented the initial interaction each merchant had with your application.

Now, Gadget introduces a high-performance entry point tailored for Shopify applications, designed to enhance the Largest Contentful Paint (LCP) time of your app. This new route will initiate the OAuth and installation process for new merchants, while seamlessly redirecting existing merchants to the / route to render the frontend experience of your application. Gadget has optimized this route to deliver the fastest possible performance, and it doesn't require activating the serverless runtime for your application, ensuring consistently low latency.

For new applications, Gadget will automatically use this URL as the default setting. However, for existing applications, you can leverage this improved route by updating your App URL to <your-app-domain>/api/shopify/install-or-render.

Customization 

If you'd like to change the behavior of your application's OAuth implementation, you can take over the App URL by adding a custom route to your application. For example, you can create a custom route at api/routes/shopify/GET-install.js

Here's an example route that implements the same behavior as Gadget's default that you can customize:

api/routes/shopify/GET-install.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{
4 Querystring: {
5 hmac: string;
6 shop: string;
7 apiKey: string;
8 host: string;
9 embedded?: "1";
10 };
11}> = async ({ request, reply, api, logger, connections }) => {
12 const { query, gadgetContext } = request;
13 const { hmac, shop: shopDomain, embedded, host: base64Host } = query;
14 const { apiKey } = gadgetContext;
15
16 if (!hmac || !shopDomain) {
17 // Before rendering this route, Gadget will automatically verify the hmac on your behalf when both of these parameters are present
18 // If either is missing, then this is not a request initiated by Shopify so we'll redirect to the root page
19 return await reply.redirect("/");
20 }
21
22 // if you have two or more apps configured for the same environment, it's possible that you could install the same shop twice on the same environment
23 // if we don't find one with this api key, going through the OAuth flow will update any existing shop record with the new api key from this app
24 const shop = (
25 await api.shopifyShop.findMany({
26 filter: {
27 myshopifyDomain: { equals: shopDomain },
28 installedViaApiKey: { equals: apiKey },
29 state: { inState: "created.installed" },
30 },
31 })
32 )[0];
33
34 // An array of all the Shopify scopes that your Gadget app requires
35 const requiredScopes = Array.from(
36 connections.shopify.configuration.requiredScopes
37 );
38
39 // This is the single entry point to your app from Shopify. It is both the route that is hit on the initial app install
40 // as well as every time a merchant clicks on your app in their admin
41 if (shop) {
42 const hasAllRequiredScopes = requiredScopes.every((requiredScope) =>
43 shop.grantedScopes?.includes(requiredScope)
44 );
45 if (embedded) {
46 return await reply.redirect("/?" + new URLSearchParams(query).toString());
47 } else {
48 const host = Buffer.from(base64Host, "base64").toString("ascii");
49 return await reply.redirect(`https://${host}/apps/${apiKey}`);
50 }
51 } else {
52 // If there's no shop record, this is a fresh install, proceed through OAuth with Shopify
53 logger.info({ shop }, "New app installation, redirecting through OAuth flow");
54 }
55
56 // This route will kick-off an OAuth flow with Shopify, pass along all parameters from Shopify (hmac, host, shop, etc)
57 const redirectURL =
58 "/api/connections/auth/shopify?" + new URLSearchParams(query).toString();
59
60 // Redirect the merchant through the OAuth flow to grant all required scopes to your Gadget app.
61 // At the end of this flow, the App URL in the connection configuration will point back to this route
62 await reply.redirect(redirectURL.toString());
63};
64
65export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{
4 Querystring: {
5 hmac: string;
6 shop: string;
7 apiKey: string;
8 host: string;
9 embedded?: "1";
10 };
11}> = async ({ request, reply, api, logger, connections }) => {
12 const { query, gadgetContext } = request;
13 const { hmac, shop: shopDomain, embedded, host: base64Host } = query;
14 const { apiKey } = gadgetContext;
15
16 if (!hmac || !shopDomain) {
17 // Before rendering this route, Gadget will automatically verify the hmac on your behalf when both of these parameters are present
18 // If either is missing, then this is not a request initiated by Shopify so we'll redirect to the root page
19 return await reply.redirect("/");
20 }
21
22 // if you have two or more apps configured for the same environment, it's possible that you could install the same shop twice on the same environment
23 // if we don't find one with this api key, going through the OAuth flow will update any existing shop record with the new api key from this app
24 const shop = (
25 await api.shopifyShop.findMany({
26 filter: {
27 myshopifyDomain: { equals: shopDomain },
28 installedViaApiKey: { equals: apiKey },
29 state: { inState: "created.installed" },
30 },
31 })
32 )[0];
33
34 // An array of all the Shopify scopes that your Gadget app requires
35 const requiredScopes = Array.from(
36 connections.shopify.configuration.requiredScopes
37 );
38
39 // This is the single entry point to your app from Shopify. It is both the route that is hit on the initial app install
40 // as well as every time a merchant clicks on your app in their admin
41 if (shop) {
42 const hasAllRequiredScopes = requiredScopes.every((requiredScope) =>
43 shop.grantedScopes?.includes(requiredScope)
44 );
45 if (embedded) {
46 return await reply.redirect("/?" + new URLSearchParams(query).toString());
47 } else {
48 const host = Buffer.from(base64Host, "base64").toString("ascii");
49 return await reply.redirect(`https://${host}/apps/${apiKey}`);
50 }
51 } else {
52 // If there's no shop record, this is a fresh install, proceed through OAuth with Shopify
53 logger.info({ shop }, "New app installation, redirecting through OAuth flow");
54 }
55
56 // This route will kick-off an OAuth flow with Shopify, pass along all parameters from Shopify (hmac, host, shop, etc)
57 const redirectURL =
58 "/api/connections/auth/shopify?" + new URLSearchParams(query).toString();
59
60 // Redirect the merchant through the OAuth flow to grant all required scopes to your Gadget app.
61 // At the end of this flow, the App URL in the connection configuration will point back to this route
62 await reply.redirect(redirectURL.toString());
63};
64
65export default route;

Shopify managed installation for Gadget apps 

Shopify managed installation allows for Shopify to install an app and update its access scopes without making any calls to the app.

This means that your users should see the following benefits:

  • Improved performance, thanks to the elimination of browser redirects
  • Faster installations and scope updates, with no screen flickering

For Gadget to work best with Shopify managed installations, your application requires @gadgetinc/react-shopify-app-bridge version 0.15.0 or greater.

Setting up Shopify managed installations 

To use Shopify managed installations with Gadget, you need a shopify.app.toml file.

If your Gadget app has a TOML file, you can move on to configuring your TOML file.

If your Gadget app does not have a TOML file, you must do the following:

  1. Connect to Shopify through the Partner dashboard
  2. Install the Shopify CLI locally
  3. Add an empty shopify.app.toml file to the root of your Gadget app
  4. Use ggt to pull down your Gadget project files locally
  5. cd into your project on your local machine and run the following command at your project root:
terminal
shopify app dev --reset

This populates your shopify.app.toml file with the necessary configuration for Shopify-managed installations. You can stop the process once the file is populated.

Configuring the shopify.app.toml file 

To use Shopify managed installation to handle OAuth for your Partner app, you must do two things within the shopify.app.toml file in your Shopify CLI app:

  1. Set the [access_scopes] configuration in the shopify.app.toml file to match the scopes selected in your Gadget app's Shopify connection
  2. Set the application_url in shopify.app.toml to the root URL of your Gadget app

Defining access_scopes 

You must add access scopes to the shopify.app.toml file of your Shopify CLI app, and these scopes must match the scopes selected in the Shopify connection settings of your Gadget app. A mismatch of scopes between the shopify.app.toml file and the Shopify connection in Gadget will result in your app not having the expected scope access to Shopify stores.

Example

In Gadget, we've selected the read_products and read_content access scopes. These scopes should also be added to the shopify.app.toml file.

A screenshot of the Setup Summary in a Gadget app, with the read_products and read_content access scopes selected and highlighted.

Add the scopes to the [access_scopes] configuration object. This ensures your app will work properly and have the expected scopes while using a Shopify managed installation.

shopify.app.toml
toml
1# additional configuration
2
3[access_scopes]
4scopes = "read_products, read_content"
5
6# additional configuration
Empty [access_scopes]?

By default, the Shopify CLI sets the [access_scopes] property to scopes = "". Leaving [access_scopes] as scopes = "" will lead to a mismatch in expected scopes between Shopify and Gadget, so make sure you have added the appropriate scopes.

If you choose not to use the Shopify managed installation, make sure you remove the [access_scopes] configuration or ignore them using the use_legacy_install_flow flag.

Setting the application_url 

With Shopify managed installations, you have to set the application_url in the shopify.app.toml file to the root URL of the Gadget app, rather than the provided /install-or-render endpoint.

Example

To enable Shopify managed installation we will point to the root URL of a Gadget app: https://sports-blog-app--development.gadget.app/.

correctly set the application_url in shopify.app.toml
toml
# additional configuration
application_url = "https://sports-blog-app--development.gadget.app"
# additional configuration
Make sure /install-or-render is not used!

Normally the application_url is set to the /install-or-render endpoint of your Gadget app: https://<app-domain>.gadget.app/api/shopify/install-or-render. If you use /install-or-render, you will no longer be using Shopify managed installations and will be introducing additional redirects into your app's installation process.

Deploy shopify.app.toml changes to Partners app 

Once you have configured your shopify.app.toml file, running the deploy command in your app root will push the changes to your Shopify Partner app:

terminal
shopify app deploy

Was this page helpful?