How to build a bundle app on Shopify (with Shopify Functions)

Time to build: ~1.5 hours

Building product bundling applications for Shopify stores is a complex task - you need an app in the Shopify admin for bundle management, a place to store and manage all of your bundle information, and changes to the Shopify storefront so the bundle information is correctly presented to shoppers. Bundling products together are a great way to increase storefront revenue, and Gadget makes it simple to set up a backend that manages all your bundle data.

Requirements

Before starting this tutorial you need the following:

A screenshot of the developer preview option with Checkout Extensibility selected when creating a new store in the Shopify Partners dashboard
Prefer a video?

Follow along to build the bundle tutorial by creating a Gadget app, getting the admin app running, and deploying a Function.

In this tutorial, you will use Gadget to build an application that can create bundles in the Shopify admin and manages your bundle information that is needed when making storefront enhancements. The provided admin app also includes a Shopify Function extension that will apply bundle discounts to your cart's line items.

You can fork this Gadget project and try it out yourself. The admin app + Function extension is available in our examples repo on Github.

You will still need to set up the Shopify Connection after forking. Continue reading to learn how to connect Gadget to Shopify!

After forking, skip to Set up the admin app.

Fork on Gadget

Bundle app inspiration: Brooklinen

Brooklinen's bundle app is used as inspiration for this tutorial. In this style of bundle app, Storefront changes are made so that it looks like only a single bundle product is added to the cart. In reality, individual line items for bundled products are added to the cart. This isn't the only way to build a product bundle application. This method was chosen for this tutorial because it allows you to track both individual product inventory and the number of times a bundle has been sold.

A gif showing a bedding bundle being added to the cart in Brooklinen's Shopify store. The cart then slides open from the right of the screen, displaying the added bundle and applied discounts

This tutorial does not include storefront changes so when testing you will add products to your cart individually. Once all products that are part of your bundle are included, the discount will be applied.

Create a Gadget app and connect to Shopify

Our first step will be to set up a Gadget project and connect our backend to a Shopify store via the Shopify connection. Create a new Gadget application.

Create a new Gadget app

To connect our Gadget backend to a Shopify store you have two options. The recommended option if you are new to Gadget and Shopify is to create a custom app via the Shopify Partners dashboard. You can also create a custom application on your Shopify store Admin page. Both of these types of custom apps have a slightly different workflow for connecting, and are detailed below:

Connect to Shopify through the Partners dashboard

Requirements

To complete this connection, you will need a Shopify Partners account as well as a store or development store

Our first step is going to be setting up a custom Shopify application in the Partners dashboard.

Both the Shopify store Admin and the Shopify Partner Dashboard have an Apps section. Ensure that you are on the Shopify Partner Dashboard before continuing.

Click on Apps link in Shopify Partners Dashboard
  • Click the Create App button
Click on Create app button
  • Click the Create app manually button and enter a name for your Shopify app
Shopify's app creation landing page in the Partners Dashboard
  • Go to the Connections page in your Gadget app
The Gadget homescreen, with the Connections link highlighted
  • Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
  • Screenshot of the Partners card selected on the Connections page
  • Click Connect on the Gadget Connections page to move to scope and model selection

Now we get to select what Shopify scopes we give our application access to, while also picking what Shopify data models we want to import into our Gadget app.

  • Enable the read and write scopes for the Shopify Products API, and select the underlying Product and Product variant models that we want to import into Gadget
Screenshot of the enabled Order read and write scopes, and selected Order model
  • You also need to enable the read and write scopes for the Shopify Discount API. You do not need any Discount models.
Screenshot of the enabled Discount read and write scopes
  • Click Confirm at the bottom of the page

We have successfully created our connection!

Now we want to connect our Gadget app to our custom app in the Partners dashboard.

  • In your Shopify app in the Partners dashboard, click on App setup in the side nav bar so you can edit the App URL and Allowed redirection URL(s) fields
  • Copy the App URL and Allowed redirection URL from the Gadget Connections page and paste them into your custom Shopify App
  • Screenshot of the connected app, with the App URL and Allowed redirection URL(s) fields

Gadget will copy the selected Shopify models, their types, validations, and associations into your Gadget backend. These models are ready to process webhooks as soon as you install the app on a Shopify store.

Custom bundle models

You need to add custom models to keep track of our bundle information. A custom Bundle model can store the following information:

  • a title
  • a percentage discount
  • a tracking product so that sales can easily be determined
  • relationships to products and product variants that make up the bundle
  • additional information such as an image for the bundle

An intermediate model, Bundle Elements, will be used to track information about products and variants that are part of a bundle. This will be built in Gadget using a has many relationship. Each Bundle Element will contain:

  • the quantity of this element included in a single bundle
  • relationships to a single product and variant that this bundle element includes in the parent bundle
  • a relationship to the bundle containing this element
The model-relationship diagram for Bundle and Bundle Element models

Add Bundle Element model

You need to create a new Bundle Element model to store data relating to individual items in a bundle.

  • Click on + to add a new model
Screenshot of the Add a Model button
  • Name your new model Bundle Element
Screenshot of the Bundle Element model name and api identifier
  • Add a new number field and name it Quantity, set the Default Value to 1, and add a Required validation
Screenshot of the Quantity Number field, with a default value of 1 and a Required validation selected

The relationship fields needed will be added in the next step.

Add Bundle model

You need to create a new Bundle model and its required fields in your Gadget app.

  • Click on + to add a model
  • Name your model Bundle
Screenshot of the Bundle model name and api identifier
  • Add a new string field, name it Title, and add a Required and Uniqueness validation
Screenshot of the Title String field with Required and Uniqueness validations
  • Add a new number field, name it Discount, add a Default Value of 0, a Required validation, and a Number Range validation with a minimum of 0 and a maximum of 100
Screenshot of the Discount Number field and the applied default of 0, with required and number range (0 -> 100) validations
  • Add a new file field and name it Image
Screenshot of the Image File field
  • Add a belongs to relationship field so that Bundle belongs to the Shopify Product model, name this field Tracker Product
Screenshot of the Tracker Product Belongs to relationship to the Shopify Product model
  • Add a has many relationship field so that Bundle has many Shopify Products through Bundle Element, name this field Products.
  • Name the relationship fields on Bundle Element Product on the Shopify Product side of the relationship and Bundle on the Bundle side of the relationship
A screenshot of the completed Has Many Through connecting Bundle to Shopify Product through Bundle Elements
  • Add another has many relationship field so that Bundle has many Shopify Product Variants through Bundle Element, name this field Variants
  • Name the relationship fields on Bundle Element Product Variant on the Shopify Product Variant side of the relationship and Bundle on the Bundle side of the relationship
A screenshot of the completed Has Many Through connecting Bundle to Shopify Product Variant through Bundle Elements

Now you have all the custom models needed to store bundle information in Gadget. There is one final step before you set up your admin app - you need to run custom code after a new Bundle is successfully created.

Run code on a Bundle create action

When a new bundle is created in Gadget, you need to save a tracker product back to Shopify, so the bundle can be surfaced in your store. You can run a custom Code Effect to write this new product, along with metafield data containing a reference to the Bundle Id, back to Shopify. To write this product back to a store:

  • Go to the Behaviour page for the Bundle model
Screenshot of the Behaviour button for the bundle model in the sidenav
  • Click the Create action tile
Screenshot of the Create tile on the Bundle behaviour page
  • Add a Success Effect
  • Select Run Code Snippet
  • Give your new code file the name createBundleProductShopify.js
Screenshot of the Create action panel with a code file named createBundleProductShopify.js set as a success effect
  • Click the Go to file button to open the code editor
Screenshot of the Go to file button for the createBundleProductShopify.js code file success effect
  • Paste this snippet into the editor
JavaScript
1const Shopify = require("shopify-api-node");
2const DISCOUNT_NAME = "Bundle Discounts";
3
4/**
5
6- Effect code for Create on Bundle
7- @param { import("gadget-server").CreateBundleActionContext } context - Everything for running this effect, like the api client, current record, params, etc
8 */
9module.exports = async ({ api, record, params, logger, connections }) => {
10 if (record.changed("title") || record.changed("bodyHTML")) {
11 // The first thing we do is create a tracker product (with a price of $0) in Shopify
12 // This will allow us to track how many times a bundle is purchased
13 const trackerProductId = record.trackerProductId;
14
15 if (!trackerProductId) {
16 const trackerProductAttributes = {
17 title: record.title,
18 price: "0.00",
19 metafields: [
20 {
21 key: "bundle_id",
22 value: String(record.id),
23 type: "single_line_text_field",
24 namespace: "gadget-bundler",
25 },
26 ],
27 };
28
29 // create tracker product and write back to Shopify
30 const newProduct = await connections.shopify.current.product.create(
31 trackerProductAttributes
32 );
33 logger.debug(
34 { bundleId: record.id, shopifyProductId: newProduct.id },
35 "create new tracker product for bundle"
36 );
37 // update our bundle in Gadget with a link to the new trackerProduct
38 await api.bundle.update(record.id, {
39 bundle: { trackerProduct: { _link: String(newProduct.id) } },
40 });
41
42 // To apply a Discount to bundled products, we need to create a new Discount in the store
43 // Bundle information is attached to this Discount in the form of a metafield
44 // This metafield will be fed into a Shopify Function extension so the Bundle discount can be applied
45
46 // update discount metafield with bundle information
47 // need to re-query to get bundle element information
48 const bundle = await api.bundle.findOne(record.id, {
49 select: {
50 id: true,
51 discount: true,
52 title: true,
53 bundleElements: {
54 edges: {
55 node: {
56 quantity: true,
57 productId: true,
58 productVariantId: true,
59 },
60 },
61 },
62 },
63 });
64 logger.debug({ bundle }, "bundle info for metafield");
65
66 // discount API not available in Gadget's default API version
67 const updatedShopifyApi = new Shopify({
68 ...connections.shopify.current.options,
69 apiVersion: "2022-10",
70 });
71
72 // the metafield definition that will be attached to the Discount for the newly created bundle
73 const discountMetafield = [
74 {
75 namespace: process.env["METAFIELD_NAMESPACE"],
76 key: process.env["METAFIELD_KEY"],
77 type: "json",
78 value: JSON.stringify(bundle),
79 },
80 ];
81
82 // create the discount object to be created in Shopify
83 const discount = {
84 functionId: process.env["FUNCTION_ID"],
85 title: `${DISCOUNT_NAME} - ${newProduct.title}`,
86 startsAt: new Date(),
87 metafields: discountMetafield,
88 };
89 logger.debug({ discount }, "discount to create");
90
91 // query to create a new discount on the current store
92 const query = `
93 mutation discountAutomaticAppCreate($automaticAppDiscount: DiscountAutomaticAppInput!) {
94 discountAutomaticAppCreate(automaticAppDiscount: $automaticAppDiscount) {
95 automaticAppDiscount {
96 title
97 }
98 userErrors {
99 field
100 message
101 }
102 }
103 }
104 `;
105
106 const discountResponse = await updatedShopifyApi.graphql(query, {
107 automaticAppDiscount: discount,
108 });
109 logger.debug({ discountResponse }, "added discount");
110 }
111 }
112};

In addition to adding tracker product metafield info and linking tracker products to your bundles, the code effect will also:

  • Query all bundle information to be serialized and sent to the Shopify Function so discounts can be applied
  • Update our Shopify API to 2022-10 so that we can use Shopify's Discount API
  • Create a new Discount
  • Store the bundle's structure as JSON within a metafield on the created discount

Set up the admin app

There is a sample bundle admin app that can be used to create new bundles. To get a copy of this project locally, run:

npm
npx [email protected] --example https://github.com/gadget-inc/examples --example-path packages/product-bundles

The bundle admin app is a Shopify CLI 3.0 app using Shopify Polaris components and the provided Gadget React tooling. It also has a Functions extension that will be used to apply discounts in your store's cart.

Before you run the admin app, you need to make changes so that your Gadget app is used as the backend.

Update your App URL

To use your local application as your admin app, you need to update the App URL for your connection. Your App URL should look like https://<your-app-slug>.gadget.app/shopify/install.

  • Go to the Connections page in Gadget
  • Click on the Edit button for your connected app
  • Change the App URL to https://localhost
Screenshot of the connected Shopify App with the Edit button highlighted

You need to make a similar change to the App URL on the Partners dashboard.

  • Navigate to your app in the Partners dashboard
  • Click on App setup
  • Change the App URL field to https://localhost
Screenshot of the changed App URL in the Shopify Partners dashboard

These are all the changes that need to be made in Gadget and the Shopify Partners dashboard. Now you need to update the admin app to use your Gadget client.

Update admin app

You need to update the admin app so that you are using your project's Gadget client. These instructions can also be found in the sample admin app's README.

  • Register the Gadget NPM registry with your local environment
npm
npm config set @gadget-client:registry https://registry.gadget.dev/npm
  • Remove the sample client from the web/frontend directory. You may need to run npm install or yarn first!
npm uninstall @gadget-client/bundle-tutorial
yarn remove @gadget-client/bundle-tutorial
  • Install your client in the web/frontend directory
npm install @gadget-client/Example App
yarn add @gadget-client/Example App
  • In web/frontend/api/gadget.ts, update the import statement at the top to use your Gadget client
JavaScript
import { Client } from "@gadget-client/Example App";

Also, update the @gadget-client import statements to use your gadget app slug in

  • web/frontend/components/AddProducts.tsx
  • web/frontend/components/VariantDetails.tsx
  • web/frontend/pages/create-bundle.tsx

You also need to add your Shopify Partners app's API key as an environment variable.

  • Go to your app on the Partners dashboard
  • Copy the API key
  • Create a .env file at the root of your admin app if it doesn't already exist
  • Enter SHOPIFY_API_KEY=<shopify-api-key> into the .env file

Deploy the Shopify Function

You now need to deploy the Shopify Function included in the admin app's extensions folder. Functions allow you to run custom backend logic in Shopify's environment to apply custom discounts. You will use this Function to apply your product bundle discount to line items in your cart.

Shopify Functions need to be written in a language that compiles down to a WebAssembly .wasm file. Shopify is primarily using Rust for their Functions tutorials and examples, but you can use any language that compiles down to .wasm. The sample Function extension in the admin app is written in AssemblyScript and looks similar to TypeScript.

Shopify Functions work by reading cart and discount metafield data from STDIN, applying custom logic to apply the desired discounts, and writing a result to STDOUT. The provided Function has 3 main code files located in extensions/bundle-tutorial-function/assembly:

  • index.ts: the primary file for the Function that reads from STDIN, applies discount logic, and writes the results to STDOUT
  • api.ts: contains classes for the Functions Product Discount API required for this tutorial
  • metafieldValue.ts: contains classes for the discount metafield that will be passed in your Function

The values being passed into your Function are declared in input.graphql.

You need to deploy the Function extension to your Partners app:

  • Build your Function locally to install the AssemblyScript compiler, navigate to extensions/bundle-tutorial-function and run npm install or yarn

  • From the root level of your admin app, run npm run deploy or yarn deploy

The Function will then run the build process specified inside the shopify.function.extension.toml file, which compiles the AssemblyScript down to a .wasm file. Once your Function is deployed, you should be able to see it in your app's Extensions in the Partners dashboard.

Screenshot of the deployed sample Function in the Shopify Partners dashboard

Once your Function is deployed, we need to make some additional changes to the Gadget app so that we can pass bundle information into the Function.

Update Gadget app

When you deployed your Function, a FUNCTION_ID was written in your app's .env file, for example SHOPIFY_BUNDLE_TUTORIAL_FUNCTION_ID=<your-key>. We need to copy this value and add it to our Gadget app as an Environment Variable.

  • Click on Environment Variables in the left navbar
  • Click Add Variable in the top right corner of the window
  • Set the variable key to SHOPIFY_BUNDLE_TUTORIAL_FUNCTION_ID
  • Copy the value from your admin app's .env file and paste as the variable value in Gadget

You also need to add the metafield namespace and key as environment variables in Gadget. These values can be found in your input.graphql file that is in your Function extension.

First, add the metafield key:

  • Click Add Variable
  • Set the variable key to METAFIELD_KEY
  • Set the variable value to function-config

Then, add the metafield namespace:

  • Click Add Variable
  • Set the variable key to METAFIELD_NAMESPACE
  • Set the variable value to product-bundles-gadget
Screenshot of Gadget's Environment Variables page with variables for the SHOPIFY_BUNDLE_TUTORIAL_FUNCTION_ID, METAFIELD_KEY, and METAFIELD_NAMESPACE

Update model permissions

If you try to run your admin app and create a bundle you will be greeted by a GGT_PERMISSION_DENIED error! We need to grant admin app users permission to our Bundle and Bundle Element models in Gadget.

  • Click Roles and Permissions in the left navbar in Gadget
  • Under the Shopify App Users role, enable the Read and Write permission for both the Bundle and Bundle Element models
Screenshot of Gadget's permissions, with Read and Write access for Bundle and Bundle Elements models selected

Run the admin app and create a bundle

Everything is now set up to be able to run your admin app and create a new bundle in your store Admin!

  • Start your local admin app with yarn dev
  • If prompted to create a new Partners app, or connect to an existing app, select connection an existing app and select your Partners app created at the beginning of this tutorial
  • Through the Partners dashboard, select a development store with Checkout Extensibility preview enabled on which to install your app
  • Sync your products and product variants to Gadget by going back to your Connections page in Gadget, clicking Shop Installs for your connection, and then Sync for the installed store
  • Go to the store on which you have installed your app, log in to the Shopify Admin, and click on Apps in the sidebar
  • Select your app from the command palette

We use the local-ssl-proxy package to create an https proxy so that your local app can be embedded in the Shopify admin. This package uses a self-signed certificate that Chrome and other browsers may block.

If you're having an issue viewing your embedded app locally, try logging in at https://localhost. If you see a NET::ERR_CERT_AUTHORITY_INVALID message for localhost you will need to click Advanced and then Proceed to localhost.

A screenshot of Chrome's NET::ERR_CERT_AUTHORITY_INVALID page.
  • Click Create a new bundle to build a new bundle
Screenshot of the admin app embedded in a development store

On the bundle creation page, you can add a title for your bundle, upload an image, and enter a discount percentage for all products in your bundle. If you click Add product you will be able to select a product and the product variants you want to add as options to your bundle. You can also choose the quantity of this product included in a single bundle.

Screenshot of a sample bundle, named Rims and Tires, being created in the admin app. An image is uploaded, a 10% discount is applied, two products are selected each with a quantity of 4, and a total price range is displayed because of the price difference between variants.

Finally, clicking Save bundle will save your new bundle to your Gadget application's database! If you go back to your Gadget application and look at the Data pages for the Bundle and Bundle Elements models, you will see the newly created bundle data.

Screenshot of the saved Bundle data for Rims and TiresScreenshot of the saved Bundle Element data for Rims and Tires

You should also see a Discount created in your Shopify Store. You can verify by going to your store's Admin and clicking on Discounts in the left navbar.

Screenshot of the Bundle Discount in the Shopify store Admin

Test a bundle purchase

If your Bundle discount appears, you are ready to test the Function!

View your online store in Shopify and add the products contained in your bundle to your cart.

You need to add the individual products in the correct quantity to your cart, not the bundle tracker product!

Once you have added all the products contained in your bundle to your cart, you can view the cart. The items in your cart will be passed to your deployed Function. If the cart contains all items included in the bundle, a bundle discount will be applied to those cart items.

Gif of a store's cart. Once an additional product is added, the bundle is complete and the items in the cart are discounted.

Congrats, you have built a product bundle application using Gadget, Shopify CLI 3.0, Shopify Functions, and AssemblyScript!

Next steps

Now that you have a sample bundle app set up, you can go about customizing it to suit your needs.

Features that could be good to implement on top of this tutorial:

  • Allow for bundle editing in the admin app
    • Use the Bundle Update action to update metafield information
  • Handle bundle deletion
    • On the Bundle Remove action, delete or deactivate the relevant Discount
    • You may need to store the Discount id as a new field on the Bundle model

Some resources to check out include: