How to build a bundle app on Shopify (with Shopify Functions)
Topics covered: Embedded Shopify apps, Access control, Storing files
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.
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.
Before starting this tutorial you need the following:
- A Shopify Partners account
- A development store with Checkout Extensibility developer preview enabled, and some sample products added

Follow along to build the bundle tutorial by creating a Gadget app, getting the admin app running, and deploying a Function.
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.
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.

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.

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
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.
- Go to the Shopify Partners dashboard
- Click on the link to the Apps page
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 the Create App button

- Click the Create app manually button and enter a name for your Shopify app

- Go to the Connections page in your Gadget app

- Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget 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

- You also need to enable the read and write scopes for the Shopify Discount API. You do not need any Discount models.

- 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

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

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

- Name your new model Bundle Element

- Add a new number field and name it Quantity, set the Default Value to 1, and add a Required validation

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

- Add a new string field, name it Title, and add a Required and Uniqueness validation

- 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

- Add a new file field and name it Image

- Add a belongs to relationship field so that Bundle belongs to the Shopify Product model, name this field Tracker Product

- 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

- 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

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

- Click the Create action tile

- Add a Success Effect
- Select Run Code Snippet
- Give your new code file the name
createBundleProductShopify.js

- Click the Go to file button to open the code editor

- Paste this snippet into the editor
1const Shopify = require("shopify-api-node");2const DISCOUNT_NAME = "Bundle Discounts";34/**56- Effect code for Create on Bundle7- @param { import("gadget-server").CreateBundleActionContext } context - Everything for running this effect, like the api client, current record, params, etc8 */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 Shopify12 // This will allow us to track how many times a bundle is purchased13 const trackerProductId = record.trackerProductId;1415 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 };2829 // create tracker product and write back to Shopify30 const newProduct = await connections.shopify.current.product.create(31 trackerProductAttributes32 );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 trackerProduct38 await api.bundle.update(record.id, {39 bundle: { trackerProduct: { _link: String(newProduct.id) } },40 });4142 // To apply a Discount to bundled products, we need to create a new Discount in the store43 // Bundle information is attached to this Discount in the form of a metafield44 // This metafield will be fed into a Shopify Function extension so the Bundle discount can be applied4546 // update discount metafield with bundle information47 // need to re-query to get bundle element information48 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");6566 // discount API not available in Gadget's default API version67 const updatedShopifyApi = new Shopify({68 ...connections.shopify.current.options,69 apiVersion: "2022-10",70 });7172 // the metafield definition that will be attached to the Discount for the newly created bundle73 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 ];8182 // create the discount object to be created in Shopify83 const discount = {84 functionId: process.env["SHOPIFY_BUNDLE_TUTORIAL_FUNCTION_ID"],85 title: `${DISCOUNT_NAME} - ${newProduct.title}`,86 startsAt: new Date(),87 metafields: discountMetafield,88 };89 logger.debug({ discount }, "discount to create");9091 // query to create a new discount on the current store92 const query = `93 mutation discountAutomaticAppCreate($automaticAppDiscount: DiscountAutomaticAppInput!) {94 discountAutomaticAppCreate(automaticAppDiscount: $automaticAppDiscount) {95 automaticAppDiscount {96 title97 }98 userErrors {99 field100 message101 }102 }103 }104 `;105106 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:
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

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

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 config set @gadget-client:registry https://registry.gadget.dev/npm
- Remove the sample client from the
web/frontend
directory. You may need to runnpm install
oryarn
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
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 STDOUTapi.ts
: contains classes for the Functions Product Discount API required for this tutorialmetafieldValue.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 runnpm install
oryarn
From the root level of your admin app, run
npm run deploy
oryarn 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.

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

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

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.

- Click Create a new bundle to build a new bundle

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.

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.


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.

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.

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:
- Gadget's Functions/AssemblyScript blog post that includes link to other code samples
- Shopify Functions API
- Shopify custom storefronts