STH
BlogHire us

← Back

How to create a Shopify app boilerplate with Firebase cloud functions

2021-03-24

This article will take you through the basic setup for building Shopify embedded apps.

  • setting up a dev environment for continuous delivery
  • setting up Shopify OAuth to exchange an access token to the Shopify Admin API

If you go through all the steps, you'll end up with a boilerplate for jump-starting Shopify embedded app development.

About Shopify embedded apps

Shopify apps can be standalone apps interacting with Shopify REST API, or they can be embedded in the merchant's admin dashboard to create an integrated app experience.

Shopify App Bridge is an official JavaScript SDK that lets you embed apps directly into the Shopify admin and Shopify POS. By using Shopify App Bridge, Shopify will automatically load your app as an iframe in the merchant's admin dashboard or POS.

(screenshot)

The stack

The boilerplate is for demonstrating the use of Shopify App Bridge and the flow of OAuth. It can also be used for quickly building a lean MVP.

I named the boilerplate Old-Fashioned because it doesn't use popular frameworks or libraries like React or Svelte. The client-side frontend is built with multi-page HTML, vanilla JavaScript, and the following libraries and tools:

  • Shopify App Bridge: embed the app in the Shopify admin dashboard
  • Uptown CSS: create UI consistent with the Shopify admin dashboard
  • Gulp 4: manage tasks like Browsersync, SCSS, and partial HTML compilation. The gulp script uses in this project is derived from this repo.

For a quick setup in the backend, we use Firebase cloud functions to interact with Shopify REST API. As for the database, there is no real need for one in this tutorial, but the boilerplate also includes the setup for a Firebase database.

Setting up the code repository

If you don't want the hassle of setting things up from scratch, you can skip ahead to Setting up in the Shopify Partner Dashboard, and use the code in the start folder of this repo to follow along.

Let's create a project folder.

mkdir old-fashioned && cd old-fashioned

Client-side

We're going to add a folder for the client-side code. This folder will be the source for our HTML, SCSS, JavaScript, and Gulp tasks. To save time from manually creating sub-folders and files, let's clone some templates.

git clone https://github.com/somethingneat/shopify-multipage-html-app-template.git

Once you have the shopify-multipage-html-app-template in the root directory, you can rename the cloned folder to something like client.

Here's the layout of the client folder:

client
├── src
		├── assets
				├── css
						├── uptown.css
				├── fonts
				├── img
				├── js
				├── partials
						├── head-scripts.html
						├── footer.html
				├── sass
		├── index.html
		├── login.html
├── gulpfile.js
├── package.json
├── env.dev.js
├── env.staging.js
├── env.prod.js

Gulp tasks

For people who are not familiar with Gulp, I'll briefly go through what's going on. Gulp lets us define tasks on what to do for certain files. For example, we can ask gulp to go through assets/sass, compile SCSS into CSS, and minimize the CSS output. The same thing is done to javascript files as well. Gulp will go through assets/js and compile the ES6 Javascript into a backwards-compatible version of JavaScript.

There are several functions in the gulpfile.js. Each function performs some sort of processing on a file type. We use the processed file type to name the function. For example, the function that compiles SCSS is simply named scss.

Partial HTMLs

This boilerplate makes use of gulp-html-partial to help us modularize HTML. We can save smaller pieces of HTML in the partials folder and use them in other HTML files like this:

<partial src="name-of-the-partial.html"></partial>

When Gulp sees the special markup, it will compile and include the partial HTML. Here's an example of using partials to modularized the footer and script tags in the <head> tag.

<html>
  <head>
    <partial src="head-scripts.html"></partial>
  </head>
  <body>
    hello world
		<partial src="footer.html"></partial>
  </body>
</html>

Environment variables

Gulp also helps us setting up environment variables. We can have three files for environment variables: env.dev.json, env.staging.json, and env.prod.json. Base on the NODE_ENV, Gulp will decide which file to use with the following code. You can find the code from line 21 to 25 in the client/gulpfile.js.

const envVars = require(`./env.${
  (isProd && "prod") ||
  (isStaging && "staging") ||
  (!isProd && !isStaging && "dev")
}.json`);

NODE_ENV id defined in npm scripts like the following code. You can find the code from line 6 to 11 in the client/package.json.

"scripts": {
  "build": "gulp",
  "start": "gulp serve",
  "prod": "cross-env NODE_ENV=prod gulp",
  "staging": "cross-env NODE_ENV=staging gulp"
}

Now we can use environment variables in HTML and JavaScript files. For example, if we have the following environment variables,

{
  "SHOPIFY_API_KEY": "your-shopify-api-key"
}

We can use it like this:

const apiKey = "{SHOPIFY_API_KEY}"

Keep in mind, these environment variables will be exposed in the front end so don't ever expose things like API secrets.

Firebase cloud functions and database

For the backend, we're going to use firebase tools to initialize a firebase project.

npm install -g firebase-tools

If you never use firebase tools before, after installing the CLI, you must authenticate. If you have trouble setting up, refer to the official documentation for more details.

firebase login

In the root directory, let's create a folder for our backend code and initialize a firebase project.

mkdir firebase && cd firebase && firebase init

We are going to need the following CLI features: database, functions and emulators.

notion image

To develop locally, we'll also need functions emulator and database emulator.

notion image

If all goes well, you should see a folder like this

firebase
├── functions
		├── node_modules
		├── index.js
		├── package.lock.json
		├── package.json
├── .firebaserc
├── database.rules.json
├── firebase.json

The file that matters to us the most is functions/index.js. This is where we code our cloud functions. For people who are not familiar with Firebase, let's briefly go through the firebase specific configuration files.

  • .firebaserc: stores your project aliases. These aliases let the CLI know where to deploy the project. You can associate multiple Firebase projects with the same project directory. For example, you might want to use one Firebase project for staging and another for production.
  • firebase.json: lists your project configuration. In our case, it specifies what ports to use for our cloud functions and database, and where to look for the database security rules.
  • database.rules.json: database rules control the access to our database. The current rule only disabled read and write access by users. We can read and write data via the firebase admin SDK.

Firing up the emulator

Let's uncomment the helloWorld code block in functions/index.js. You'll end up with the following code.

const functions = require("firebase-functions");

exports.helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});

Navigate to the functions folder, and run the following command:

firebase emulators:start

You'll see something like this in your terminal.

notion image

If we visit the link given in the terminal: http://localhost:5001/old-fashioned-boilerplate/us-central1/helloWorld, we'll be greeted with "Hello from Firebase!" in the browser.

Setting up environment variables

Here's how to set up environment variables using the CLI:

firebase functions:config:set env.key="API_KEY" env.secret="API_SECRET"

And we can use them in the index.js like so:

const API_KEY  = firebase.config().env.key;
const API_SECRET = firebase.config().env.secret;

Keep in mind that by running the above command, we set the environment variables for the current project alias. If we add an alias for a production environment, we'll have to re-do the settings.

Bulk update

Let's organize environment variables in a file. In the functions folder, let's create a file named env.dev.json and input the following key-value pairs.

{
  "shopify": {
    "client": {
      "secret": "your-secret",
      "key": "your-api-key"
    }
  },
  "test": {
    "emulating": "true"
  }
}

Now we can deploy the variables by running the following command:

firebase functions:config:set env="$(cat env.dev.json)"

We can also set up another file name env.prod.json to deploy variables to the production project. Unlike the environment variables in the frontend, we're going to use credentials like API secret in the Firebase cloud function so this env file must be stored privately.

Using environment variables in the local emulator

The local emulator looks environment variables from a file named .runtimeconfig.json inside the functions directory. We can manually create the file and paste the JSON object or we can run the following command to generate the file:

firebase functions:config:get > .runtimeconfig.json

Setting up in the Shopify Partner Dashboard

A Shopify app must be accessible from the public internet because the app authenticates using OAuth in order to get access to Shopify's API resources. After a user authorizes our app, Shopify will redirect the user back to an endpoint in our app with an authorization code. We can use this code to exchange a permanent access token.

This requirement is easily met in a production environment where everything is deployed to the cloud with accessible endpoints. However, we are still in development and would like to run our app locally. Shopify cannot redirect to http://localhost:5001/old-fashioned-boilerplate/us-central1/oauthCallback. A solution is to expose our localhost.

Exposing your localhost

To make localhost public, we need to create a secure HTTP tunnel using a proxy server. Worry not, there are several tools out there for this so we don't have to mess with things like nginx.

I personally use ngrok and Shopify also recommends using ngrok. The downside is that the free version of ngrok doesn't allow custom subdomains for tunnels. Every time you make a tunnel, it generates a random subdomain. There are several other alternatives, but they most likely charge for custom domains as well.

Let's get started. In one terminal, go to the client folder and fire up gulp in one terminal.

npm run start

In another terminal go to the firebase/function folder and fire up the firebase emulator.

firebase emulators:start

In a new terminal, install ngrok.

npm install ngrok -g

Set ngrok to use port 3000, the port where Gulp serves up the frontend resources.

ngrok http 3000

If you go to the HTTPS version of the forwarding URL, you'll see the same "Hello World" message.

notion image

In another terminal, set ngrok to use port 5001, the post where Firebase serves up the cloud functions.

ngrok http 5001

If you go to the displayed forwarding URL and add a path of /old-fashioned-boilerplate/us-central1/helloWorld, you'll see the "Hello from Firebase!" message sent from our helloWorld cloud function.

notion image

Creating a public app in Shopify

Now that we have our app set up, let's get it working as a Shopify app.

Let's create an app in the Shopify Partner Dashboard. If you don't have a partner account yet, you can sign up here.

Go to the Apps section and click on Create app. You'll be prompted to choose what type of app you want to create. You can learn about different app types here. We're going to create a public app.

notion image

After naming the app, copy the ngrok forwarding URL of our frontend resources into the App URL field and add /login.html to the end of the path. Copy the ngrok forwarding URL of the cloud functions into the Allowed redirection URL(s) field and add /old-fashioned-boilerplate/us-central1/oauthCallback to the end of the path. Shopify will redirect a merchant to this path once she authorizes our app. We will write a cloud function name oauthCallback to get an access token.

Click on Create app.

notion image

Adding Shopify API keys

Copy the Shopify API key and Shopify API secret key to firebase/functions/env.dev.json.

Copy the Shopify API key into the SHOPIFY_API_KEY prop in client/env.dev.json.

Make sure you've updated the configuration by running:

firebase functions:config:set env="$(cat env.dev.json)"
firebase functions:config:get > .runtimeconfig.json

Embedding the app in the Shopify Admin Dashboard

Before we get started, make sure you've done these:

  • You have a basic understanding of OAuth. I found this article extremely helpful.

When to initiate the OAuth dance

Now let's walk through what our app needs to do when a store installs the app and lands on the login.html.

We should check whether the store already grants us access to its Shopify Admin API resources. If so, we don't have to go through OAuth to gain access.

When a store grants a third-party app access to its resources, Shopify will give the app an access token. This access token is permanent and specific to the app and the store. Once we obtain one, we should store it somewhere. In this tutorial, we'll store the access token in cookies.

If our app gets an access token from cookies, it needs to check the validity of the token by pinging the Shopify Admin API. If the token is invalid, the app will go through the OAuth dance. If the token is valid, the app will redirect the merchant to the index.html.

If the app doesn't have the access token yet, it will go through the OAuth dance to obtain one.

Here's the pseudo code of the whole process.

const token = getCookie("access_token");

if (token) {
	pingShopifyAdminAPI()
    .then(goToIndex)
    .catch(oauth);
} else {
	oauth()
}

Copy the code below into the client/src/login.html. Let's break down what's going on here.

<html>
  <head>
    <partial src="head-scripts.html"></partial>
    <script>
	    // code block 1
			const AppBridge = window["app-bridge"];
			const createApp = AppBridge.default;
			const apiKey = "{SHOPIFY_API_KEY}";
			const urlParams = new URLSearchParams(window.location.search);
			const shopOrigin = urlParams.get("shop");
			const app = createApp({
			  apiKey: apiKey,
			  shopOrigin: shopOrigin,
			});
			const Redirect = AppBridge.actions.Redirect;
		  const redirect = Redirect.create(app);
			
			// code block 2
			const token = getCookie("access_token");
			if (token) {
			  pingShopifyAdminAPI()
			    .then(goToIndex)
			    .catch(oauth);
			} else {
			  oauth();
			}
			
			// code block 3
			function pingShopifyAdminAPI() {
				// ...
			}
			
			// code block 4
			function oauth() {
			  // ...
			}
			
			// code block 5
			function goToIndex() {
			  // ...
			}
    </script>
  </head>

  <body>
    <div class="main">
      <div class="content">
        <div class="loading">
          <div class="lds-ellipsis">
            <div></div>
            <div></div>
            <div></div>
            <div></div>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

Inside the head-scripts.html partial, there is a <script> tag pointing to a CDN-hosted copy of Shopify App Bridge.

In code block 1, we initialize the app bridge so the login page is embedded inside the Shopify Admin Dashboard. The initialization requires our Shopify API key and the store's .myshopify.com domain. The .myshopify.com domain is contained in the shop URL query parameter that’s appended to your application URL when your app is loaded inside the Shopify admin. We also initialize the redirect action of the app bridge, which allows us to modify the top-level browser URL and the local app URL.

Code block 2 is the pseudo-code shown above.

The permission redirect

Let's work on the oauth function first since we don't have an access token at this point.

The oauth function needs to generate a permission URL, and redirect the merchant to the URL. The merchant's screen will look like this:

notion image

The permission URL is structured like this:

https://{shop}.myshopify.com/admin/oauth/authorize?client_id={api_key}&scope={scopes}&redirect_uri={redirect_uri}

Once the merchant clicks on Install unlisted app, Shopify will redirect to the redirect_uri defined in the permission URL. In this case, the redirect URI points to the oauthCallback cloud function.

function oauth() {
  const redirectUri = `https://168fd7e2ab77.ngrok.io` +
    `/old-fashioned-boilerplate/us-central1/oauthCallback`;
  const permissionUrl =
    `/oauth/authorize?client_id=${apiKey}` +
    `&scope=read_script_tags,write_script_tags&redirect_uri=${redirectUri}`;
  // If the current window is the 'parent', change the URL by setting location.href
	if (window.top == window.self) {
    window.location.assign(`https://${shopOrigin}/admin${permissionUrl}`);
	// If the current window is the 'child', change the parent's URL with ShopifyApp.redirect  
	} else {
    redirect.dispatch(Redirect.Action.ADMIN_PATH, permissionUrl);
  }
}

The oauthCallback cloud function

A few query parameters are attached to the redirect URL when the merchant installs and Shopify redirects. Here are the ones that we're going to use in this tutorial.

  • code: An authorization code for obtaining an access token;
  • hmac: A Hash-based Message Authentication Code for verification;
  • shop: The shop to be authenticated.

In the oauthCallback cloud function, we first make sure we have the above query parameters, and then we need to verify that the request is indeed from Shopify, not some malicious third party. We verify the authenticity of the request using HMAC and our API secret key. The detailed process is documented in this Shopify tutorial. We'll implement a NodeJS version of what's described in the Shopify tutorial.

If everything checks out, we can use the code to exchange an access token from the Shopify Admin API. To keep a record of installation, we can also store the shop and its access token in our database. Finally, we'll redirect the merchant to the index.html.

Here's the pseudo-code of the process:

const { shop, code, hmac } = req.query;
if (!(shop && hmac && code)) {
	return redirectToErrorPage()
}

const hashEquals = validateHMAC({
  reqQuery: req.query,
  hmac,
  apiSecret: SHOPIFY_CLIENT_SECRET,
});
if (!hashEquals) return redirectToErrorPage();

return getAccessToken()
	.then((res) => {
		const accessToken = res["access_token"];
		return storeShopAndAccessToken({shop, accessToken})
			.then(redirectToIndexPage)
	})
	.catch(redirectToErrorPage)

Copy the following code and replace the code currently in firebase/function/index.js. This is the implementation of the pseudo-code shown above. Make sure you've npm installed request, request-promise and firebase-admin.

// code block 1
const functions = require("firebase-functions");
const rp = require("request-promise");
const admin = require("firebase-admin");
const { validateHMAC } = require("./utils");
const SHOPIFY_CLIENT_SECRET = functions.config().env.shopify.client.secret;
const SHOPIFY_CLIENT_ID = functions.config().env.shopify.client.key;
const EMULATING = functions.config().env.test.emulating === "true";

// code block 2
const app = admin.initializeApp(
  EMULATING
    ? {
        projectId: "old-fashioned-boilerplate",
        databaseURL: "http://localhost:9000/?ns=old-fashioned-boilerplate",
      }
    : {}
);
const database = app.database();

exports.oauthCallback = functions.https.onRequest((req, res) => {
	// code block 3
  res.set("Access-Control-Allow-Origin", "*");
  res.set("Access-Control-Allow-Methods", "GET, OPTIONS");
  res.set("Access-Control-Allow-Headers", "*");

	// code block 4
  const shop = req.query.shop;
  const code = req.query.code;
  const hmac = req.query.hmac;
  if (!(shop && hmac && code))
    return res.redirect(
      `https://${shop}/admin/apps/${SHOPIFY_CLIENT_ID}/error.html`
    );
	
	// code block 5
  const hashEquals = validateHMAC({
    reqQuery: req.query,
    hmac,
    apiSecret: SHOPIFY_CLIENT_SECRET,
  });
  if (!hashEquals)
    return res.redirect(
      `https://${shop}/admin/apps/${SHOPIFY_CLIENT_ID}/error.html?message=HMAC validation failed`
    );
	
	// code block 6
  return rp
    .post({
      uri: `https://${shop}/admin/oauth/access_token`,
      form: {
        code: code,
        client_id: SHOPIFY_CLIENT_ID,
        client_secret: SHOPIFY_CLIENT_SECRET,
      },
      transform: JSON.parse,
    })
    .then((body) => {
      const accessToken = body["access_token"];
      if (!accessToken)
        return res.redirect(
          `https://${shop}/admin/apps/${SHOPIFY_CLIENT_ID}/error.html`
        );
      return database
        .ref(`${shop.split(".myshopify.com")[0]}`)
        .once("value")
        .then((snap) => {
          const shopInDb = snap.val();
          if (!shopInDb)
            return database.ref(`${shop.split(".myshopify.com")[0]}`).set({
              installDate: Date.now(),
              accessToken,
            });
          else
            return database
              .ref(`${shop.split(".myshopify.com")[0]}`)
              .child("accessToken")
              .set(accessToken);
        })
        .then(() =>
          res.redirect(
            `https://${shop}/admin/apps/${SHOPIFY_CLIENT_ID}` +
              `/index.html?access_token=${accessToken}&shop=${shop}`
          )
        );
    })
    .catch((err) => {
      return res.redirect(
        `https://${shop}/admin/apps/${SHOPIFY_CLIENT_ID}/error.html`
      );
    });
});

In code block 1, we import necessary functions and env variables.

In code block 2, we initialize an firebase app with firebase-admin. Since our database doesn't allow read and write access by users. We can only read and write data via the firebase-admin SDK. If the cloud functions are in production, the firebase-admin SDK will automatically detect the firebase project configurations so we put an empty object {} as the argument for admin.initializeApp. However, we do need to manually specified the settings like the project id and database URL for the local emulator.

Code block 3 enables CORS.

In code block 4, we check for the necessary query parameters.

In code block 5, we verify the origin of the request with a function named validateHMAC. We'll code this function in a bit.

In code block 6, we get the access token, store it in the firebase database, and then redirect the merchant to the index page of our app.

Now the only missing piece is the validateHMAC function. In firebase/functions, create a utils folder, create an index.js file in the utils folder, and paste in the following code.

const crypto = require("crypto");
const querystring = require("querystring");

const validateHMAC = ({ reqQuery, hmac, apiSecret }) => {
  const map = Object.assign({}, reqQuery);
  delete map["hmac"];
  const message = querystring.stringify(map);
  const providedHmac = Buffer.from(hmac, "utf-8");
  const generatedHash = Buffer.from(
    crypto
      .createHmac("sha256", apiSecret)
      .update(message)
      .digest("hex"),
    "utf-8"
  );

  let hashEquals = false;
  try {
    hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac);
  } catch (e) {
    hashEquals = false;
  }

  return hashEquals;
};

module.exports = {
  validateHMAC
};

Experiencing the OAuth flow in a development store

Now we have implemented the OAuth flow for obtaining an access token. Let's see it in action in Shopify. First, copy the following code into client/src/index.html so that when the oauthCallback cloud function redirects back to our app's index, we'll be greeted with an embedded app.

<html>
  <head>
    <partial src="head-scripts.html"></partial>
    <script type="text/javascript">
      const urlParams = new URLSearchParams(window.location.search);
      const AppBridge = window["app-bridge"];
      const createApp = AppBridge.default;
      const app = createApp({
        apiKey: `{SHOPIFY_API_KEY}`,
        shopOrigin: urlParams.get("shop"),
      });
      setCookie("access_token", urlParams.get("access_token"));
    </script>
  </head>
  <body>
    We got the access token!
    <partial src="footer.html"></partial>
  </body>
</html>

Go to the app page in your Shopify Partner Dashboard. Click on Test on development store under More actions.

notion image

Pick a development store.

notion image

Shopify loads the login.html page of our app, which runs the oauth function. The oauth function makes a permission redirect.

notion image

Once we click on Install unlisted app, Shopify redirect to the oauthCallback cloud function. The terminal running the firebase emulator should look like this:

notion image

After obtaining an access token, the oauthCallback function redirects us to the index page.

notion image

Let's go to the database emulator UI URL shown in the firebase emulator terminal.

notion image

We should see a key-value pair of our development store and its access token.

notion image

Finishing up the code

Let's add another cloud function to firebase/functions/index.js. We'll use this cloud function to interact with the Shopify Admin API. This endpoint takes three query parameters:

  • shop: The shop we're working with.
  • token: The access token.
  • resource: The type of data we need, i.e. products, orders, metafields...etc. See the official documentation for more details
exports.getResource = functions.https.onRequest((req, res) => {
  res.set("Access-Control-Allow-Origin", "*");
  res.set("Access-Control-Allow-Methods", "GET, OPTIONS");
  res.set("Access-Control-Allow-Headers", "*");

  const shop = req.query.shop;
  const token = req.query.token;
  const resource = req.query.resource;

  return rp({
    uri: `https://${shop}/admin/api/2019-07/${resource}.json`,
    headers: { "X-Shopify-Access-Token": token },
    method: "GET",
    json: true,
  })
    .then((body) => res.send(body))
    .catch((err) => {
      return res.sendStatus((err && err.statusCode) || 500);
    });
});

Fill in the empty functions in client/src/login.html.

// code block 3
function pingShopifyAdminAPI() {
  return fetch(
	    `https://168fd7e2ab77.ngrok.io/old-fashioned-boilerplate/us-central1` +
	      `/getResource?shop=${shopOrigin}&token=${token}&resource=shop`
	  )
	    .then((response) => response.json())
}

// code block 5
function goToIndex() {
  const indexUrl =`/index.html?shop=${shopOrigin}&access_token=${token}`
  if (window.top == window.self) {
    window.location.assign(`https://${shopOrigin}/admin${indexUrl}`);
  } else {
    redirect.dispatch(Redirect.Action.APP, indexUrl);
  }
}

Now when we access the app from the development store, we don't have to go through the OAuth. After the app validates the access token stored in cookies, we'll be redirected to the index page.

Wrapping up

This article has gone through a lot of code but the final boilerplate is fairly simple. The boilerplate is perfect for building small apps with a focused set of features. For example, here's one of my app that's built on the old-fashioned boilerplate. It does one thing only: redirecting visitors based on their location.

Thank you for reading. I hope this article has shed some light on getting started with Shopify app development. We have barely scratched the surface. There is a lot of room for improvement like:

  • using Github action to deploy the Firebase project and serve the frontend from AWS S3
  • writing boilerplate functions that handle billing and script tag installation

Github repo

 
Privacy policyCookie policy
Using this website you adhere to our Privacy Policy and Cookie Policy. © 2021, All Rights Reserved