# Building Shopify apps ## Where and how Shopify data is saved All of the Shopify fields are mapped to a corresponding field in your Gadget model, and then these Gadget model records are updated when Shopify sends webhooks, or you sync data. You can identify these fields by looking at the schema for any of your app's Shopify models and finding any field with the Shopify icon. When a record in Shopify is changed, Shopify sends a webhook to your Gadget application, which triggers the appropriate model Action. A model's `create` webhook will fire the `create` model Action, `update` will fire `update`, and `delete` will fire `delete`. All of the incoming fields from the webhook are mapped and stored in their corresponding field in Gadget. Syncs work much like webhooks in that they will create or update records in Gadget. However, Gadget currently does not delete any models that exist in Gadget but no longer exist in Shopify. Gadget does not automatically write data to the Shopify database. If you want to make changes to Shopify data, you should make API calls to the Shopify API from within your Gadget actions. See for more details. ## Shopify models Once set up, all of the models based on your selected scopes are now part of your app's GraphQL API, and you can query them like any other model in your app. You can also add your own fields to these models. If you want to store extra data about each record from Shopify, Gadget will happily support adding a new field to the Shopify model with any of Gadget's supported field types, including relationships, and file fields. Fields added to Shopify models can also be marked as [metafields](https://docs.gadget.dev/guides/plugins/shopify/advanced-topics/metafields-metaobjects) for syncing from Shopify. ### Shopify model actions You can extend the `create`, `update`, and `delete` [actions](https://docs.gadget.dev/guides/actions) on Shopify models. Extending these actions allows you to react to changes within Shopify. You may also add your own actions, allowing you to define your own business logic to manipulate any data in the context of the record. For example, you can react to a product being created by adding a code to the `create` action on the `shopifyProduct` model: ```typescript export const onSuccess: ActionOnSuccess = async ({ record, logger }) => { logger.info({ record }, "shopify product was just created"); }; ``` Gadget will run the `create` action for this model any time a `product/create` webhook is emitted by Shopify or if a new Product record is discovered during a sync. You can also add a custom action to the `shopifyProduct` model to run additional business logic, triggerable by you instead of a webhook. For example, you can add an action to update a product's title to be all uppercase. To do this, you would add a new file in the `actions` folder of the model, and implement the `run` function: ```typescript export const run: ActionRun = async ({ record, connections }) => { const shopify = connections.shopify.current; if (shopify && record.changed("title") && record.title) { await shopify.graphql( `mutation ($input: ProductInput!) { productUpdate(input: $input) { product { title } userErrors { message } } }`, { input: { id: `gid://shopify/Product/${record.id}`, title: record.title.toUpperCase(), }, } ); } }; ``` This action would be available to run on any `shopifyProduct` record in your database. ### Removing default model actions You can also remove the existing `create`, `update`, or `delete` actions from your models, but this will prevent the connection from being able to perform those actions in response to webhooks/syncs. For example, if you remove the `delete` action from the `shopifyProduct` model, your Gadget database will no longer remove products when Shopify sends `products/delete` webhooks, preserving a long-term record of all products ever seen. Gadget doesn't prevent you from deleting models, but doing so may prevent data from being updated. For example, if you have a product and product variant model, deleting the product model will mean that you no longer get updates to variant records. Deleting the product variant model is fine, and will not prevent products from being synced. ### Key Shopify models In addition to the models available to you based on your selected scopes, the Shopify connection creates three additional models: `shopifyShop`, `shopifySync`, and `shopifyGDPRRequest`. #### `shopifyShop` The `shopifyShop` model is a representation of a Shopify shop connected to Gadget via the Shopify connection. It stores information about the connected shop, and a record is created when a shop is connected for the first time. Logic that should run on app installation can be added to the `install` and/or `reinstall` actions. For example, you may want to run an initial data sync when a merchant installs your application. Take a look at our [syncing](https://docs.gadget.dev/guides/plugins/shopify/syncing-shopify-data) docs for more details. #### `shopifySync` The `shopifySync` model represents the data sync between Shopify and Gadget for any given connected Shopify shop. Like the `shopifyShop` model, you can run logic on the creation of a sync record, which will execute whenever a sync is initiated between Shopify and Gadget. #### `shopifyGdprRequest` The `shopifyGdprRequest` model represents requests from Shopify's [mandatory webhooks](https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks) in compliance with General Data Protection Regulation (GDPR). You can add custom code to the `shopifyGdprRequest` model's `create` action to handle the request as needed, ensuring your app is GDPR compliant. To view your GDPR webhook URLs, or read more about the Shopify plugin's GDPR functionality, see . ## Protected customer data access (PCDA) You will need to ensure that you request access to **protected customer data** if you are using one of the following models: * Shopify Balance Transaction * Shopify Cart * Shopify Checkout * Shopify Comment * Shopify Customer * Shopify Draft Order * Shopify Event * Shopify Fulfillment Order * Shopify Order * Shopify Company The protected customer data access form is only required for **public apps** or for apps without a distribution method set. If you have selected the **custom** distribution option for your app in the Dev Dashboard, you do not need to complete the form and it will not appear as an option in the Shopify Partner dashboard. To request access to protected customer data: 1. Go to your app's page in the [Shopify Dev Dashboard](https://dev.shopify.com/dashboard). 2. Select a distribution method. This will bring you to the Shopify Partner dashboard. 3. Click on **API access requests** in the left nav and find the section labeled **Protected customer data access**. Fill out the form. See [Shopify's documentation](https://shopify.dev/apps/store/data-protection/protected-customer-data#protected-customer-data-api-resources) for more details. If your app needs access to fields such as **customer name, email, phone number, or address,** you need to make sure you request individual field access in the optional _Protected customer fields_ section of the customer data access page. If you do not select the individual fields you need access to, Shopify will not send this data via sync or webhook, and the records in your Gadget app database will have null or empty values instead. ## Installing your app on a dev Store When testing your Shopify app, you can only install it on development stores that have been specifically set up with transfers disabled. This is because development stores that can be transferred to merchants have specific limitations around app installation. You can set up a new development store for testing on the [Shopify Dev Dashboard](https://dev.shopify.com/dashboard). Gadget will also redirect you to the development store creation form if you set up a Shopify connection without a development store in your Shopify organization. If you try to install your app on a development store that was created with transfers enabled (i.e., created using "Create a store for a client" option), you'll need to select a distribution method for your app first. This is a Shopify requirement to ensure proper app distribution and merchant experience. For more details about development store types and their limitations, see [Shopify's documentation on development stores](https://shopify.dev/docs/api/development-stores#create-a-development-store-to-test-your-app). **Note**: Development stores created for testing cannot be transferred to merchants later, so make sure to choose the correct type of development store based on your needs. ## Adding code to actions Shopify models can be given business logic just like any other model in Gadget using [model actions](https://docs.gadget.dev/guides/actions). Most often, developers add code to the `onSuccess` function of the default model actions (`create`, `update`, or `delete`) on a Shopify model. Within action functions, you have access to: * the current `record` being manipulated * the incoming `params` from webhooks * your `api` client object for fetching or changing other data in your app * the `connections` object for making API calls back to Shopify plus other information provided by the [action context](https://docs.gadget.dev/guides/actions/code#action-context). **Note:** Gadget's sync logic replicates changes made in Shopify to your Gadget app, but not the other way around. If you want to update Shopify data, you must make explicit API calls to the Shopify API to do so. Let's say we wanted to build an automatic product tagger for Shopify products. When a product is changed in Shopify, our Gadget app should parse the product body, extract some new tags, and send them back to Shopify so customers can search for the product on the storefront easily. To do this, we can add code to the `shopifyProduct` model's `update` action: ```typescript import { extractKeywords } from "easy-keywords"; export const onSuccess: ActionOnSuccess = async ({ api, record, logger, connections, }) => { const shopify = connections.shopify.current; if (shopify && record.body !== null && record.changed("body")) { const newTags = (await extractKeywords(record.body)).slice(20); const allowedTags = (await api.allowedTag.findMany()).map( (record) => record.tag ); logger.info({ newTags }, "saving new tags"); await shopify.graphql( `mutation ($input: ProductInput!) { productUpdate(input: $input) { product { title } userErrors { message } } }`, { input: { id: `gid://shopify/Product/${record.id}`, tags: newTags, }, } ); } }; ``` The above code extracts tags from the product's `body` field using a module from npm, gets an authenticated API client for working with a specific Shopify shop's API, and then updates the product by making a call back to Shopify using that client. ### Accessing the Shopify API Follow our [tutorials](https://docs.gadget.dev/guides/plugins/shopify/quickstart) and learn how to connect Gadget and Shopify in just a few minutes! Gadget provides a helper for quickly accessing a client API object for communicating with Shopify. Gadget uses the [`shopify-api-node` library](https://www.npmjs.com/package/shopify-api-node) as it is battle-tested, has good coverage of the Shopify API, and has support for automatic retries to avoid the Shopify API rate limits. To access a Shopify API client, use the `connections.shopify` object in your action code. `connections.shopify` is a helper object with some handy functions for accessing already-created API Client objects: * `connections.shopify.current` returns a Shopify API client for the current shop if there is one in context. For Public Shopify apps making requests from an embedded app, Gadget is able to track the shop making the request and populate this value. * `connections.shopify.forShopId` allows creating a `Shopify` client instance for a specific shop ID * `connections.shopify.forShopDomain` allows creating a `Shopify` client instance for a specific myshopify domain. For example, we can use `connections.shopify` to access a Shopify client for a given store and then make a call to the Products GraphQL API in Shopify to create a new product record: ```typescript export const onSuccess: ActionOnSuccess = async ({ connections }) => { const desiredShopifyStoreDomain = "the-store.myshopify.com"; const shopifyClient = await connections.shopify.forShopDomain( desiredShopifyStoreDomain ); await shopifyClient.graphql( `mutation ($input: ProductInput!) { productCreate(input: $input) { product { title descriptionHtml tags } } userErrors { message } }`, { input: { title: "New Product", descriptionHtml: "This is the latest product on The Store", tags: ["product", "new"], }, } ); }; ``` If you prefer to use another client, the provided client has the necessary token information: ```typescript import MyShopifyClient from "my-shopify-client"; export const onSuccess: ActionOnSuccess = async ({ connections }) => { const desiredShopifyStoreDomain = "the-store.myshopify.com"; const shopifyClient = await connections.shopify.forShopDomain( desiredShopifyStoreDomain ); const accessToken = shopifyClient.storefrontAccessToken; const { shop } = await shopifyClient.graphql(`{ shop { myshopifyDomain } }`); const myClient = new MyShopifyClient({ myshopifyDomain: shop.myshopifyDomain, accessToken, }); // ... }; ``` ### Querying Shopify's GraphQL API The `shopify-api-node` library has a `graphql` method that allows you to make GraphQL queries to Shopify. Here's a simple example that queries for the current shop's name: ```typescript export const onSuccess: ActionOnSuccess = async ({ connections }) => { // https://shopify.dev/docs/api/admin-graphql/2024-04/queries/shop // get the shopify-api-node client for the current shop const shopify = connections.shopify.current; if (shopify) { // use the client to make the GraphQL call await shopify.graphql(` query { shop { name } } `); } }; ``` This is a more complex example that creates a new discount for the current shop: ```typescript export const onSuccess: ActionOnSuccess = async ({ connections }) => { // https://shopify.dev/docs/api/admin-graphql/2024-04/mutations/discountAutomaticAppCreate // get the shopify-api-node client for the current shop const shopify = connections.shopify.current; if (shopify) { // use the client to make the GraphQL call await shopify.graphql( `mutation ($automaticAppDiscount: DiscountAutomaticAppInput!) { discountAutomaticAppCreate(automaticAppDiscount: $automaticAppDiscount) { automaticAppDiscount { discountId title } userErrors { message } } }`, { automaticAppDiscount: { functionId: process.env["FUNCTION_ID"], title: "my new discount", startsAt: new Date(), }, } ); } }; ``` ### When to make API calls to Shopify in actions **Reading data:** Prefer reading from your Gadget database to avoid rate limits and improve performance. **Writing data:** Place API calls in the `onSuccess` function. Gadget executes the `run` function inside a database transaction. If `run` fails, database changes are rolled back. Since Gadget cannot roll back changes to external systems like Shopify, API calls made in a failing `run` function will persist, potentially causing your systems to get out of sync. ### Managing Shopify API rate limits Shopify has [restrictive rate limits](https://shopify.dev/api/usage/rate-limits), limiting apps to making as few as 4 requests per second to any given shop. **The best way to avoid rate limits on read is to read data from your Gadget database.** Gadget's Shopify connection syncs data from Shopify to your Gadget database so you can read quickly and without rate limits. When writing to Shopify, or to query data manually, use Gadget's built-in [background actions](https://docs.gadget.dev/guides/actions/background) to intelligently manage rate limits. When a background action is enqueued with `shopifyShop` context, Gadget will automatically use the adaptive rate limiter to manage the requests made to Shopify for that shop. There are two ways to enqueue a background action with `shopifyShop` context: * , which is required when you need to manipulate the result of a Shopify API call in your action. * , which is best when you do not need the result of the Shopify request, and just want a resilient write to Shopify. It is important to understand the difference between these two methods and when to use each. #### Enqueue a background action that calls the Shopify API ```typescript export const onSuccess: ActionOnSuccess = async ({ api, connections }) => { // the processCustomOrders action calls the Shopify API await api.enqueue(api.processCustomOrders, { // pass the shopifyShop ID to the background action shopifyShop: connections.shopify.currentShopId, // ... }); }; ``` This will enqueue the **entire** action. It will be run when Gadget's adaptive rate limiter determines it is most likely to not hit Shopify's rate limits. Once the action is dispatched, the action behaves and runs normally. **Use this when you need to manipulate the result of a Shopify API call in your action.** There is still a chance that you get a 429 response when your action runs and calls Shopify. **Enqueuing an action with `shopifyShop` context works best when the action calls Shopify right away**. If an action has additional requests and long running processes that occur before calling Shopify, try to split up the work and enqueue the Shopify request with `shopifyShop` context separately. #### Enqueue a Shopify GraphQL query directly ```typescript export const onSuccess: ActionOnSuccess = async ({ api, connections }) => { const shopify = connections.shopify.current; // enqueue the productCreate mutation await api.enqueue(shopify.graphql, { query: `mutation ($input: ProductCreateInput!) { productCreate(product: $input) { product { id title } userErrors { field message } } }`, variables: { input: { title: "Cool socks", }, }, }); }; ``` This is useful for resilient mutations and writing data to Shopify. **If you do not need to manipulate the result of the Shopify request, this is the method you should use to hook into the managed rate limiter.** Gadget uses a PID (Proportional-Integral-Derivative) controller to dynamically adjust the rate of requests sent to Shopify. The controller monitors the Shopify API's response times and error rates, and adjusts the request rate in real-time to maximize throughput without exceeding the limits. #### Manually handle rate limits The [`shopify-api-node`](https://www.npmjs.com/package/shopify-api-node) object returned by Gadget's `connections.shopify` helper is also pre-configured to retry when encountering rate limit errors. `shopify-api-node` will retry requests, respecting Shopify's `Retry-After` header. The number of times `connections.shopify` will retry a request is configurable. By default it will retry 2 times (or 6 times if your app is on a Gadget version prior to `v1.1`). If you would like to configure how many times it will retry you can do so on an action-by-action basis. ```typescript export const run: ActionRun = async ({ record, connections }) => { // this will retry the request 10 times before failing connections.shopify.maxRetries = 10; await connections.shopify.current?.graphql( `mutation ($input: ProductInput!) { productUpdate(input: $input) { product { id tags } userErrors { message } } }`, { input: { id: `gid://shopify/Product/${record.id}`, tags: ["foo", "bar", "baz"], }, } ); }; ``` When `connections.shopify` retries a request to Shopify, it will wait for the `Retry-After` header to determine how long to wait before retrying. This can lead to a request taking longer than expected to complete and increase the amount of request time your app uses. If you are repeatedly hitting Shopify's rate limit you may want to consider running your action as a to make sure that you are only running actions when it likely that they will succeed. ### Current shop tenancy in actions As the frontend is making queries and performing mutations on the data in your Gadget application, it is important to make sure that cross-talk between different shops is not allowed. This is especially relevant for public Shopify apps. The context of the current shop is determined by the user and session making the request, or, if the action is triggered by a webhook or sync, the current record that is being processed. The following information about the current shop is accessible from the `connections.shopify` object supplied in the action context: | | | | --- | --- | | `connections.shopify.currentShopId` | The shop id of the current shop | | `connections.shopify.currentShopDomain` | The myshopify domain of the the current shop | | `connections.shopify.currentClientId` | The client ID of the Shopify app that the current shop installed | | `connections.shopify.currentClientSecret` | The client secret of the Shopify app that the current shop installed | | `connections.shopify.currentSession.token` | The Shopify session token that was used to authenticate the current shop for the current request | | `connections.shopify.currentSession.userId` | The Shopify user id attached to the Shopify session token that was used to authenticate the current shop for the current request | `connections.shopify.currentShopId` The shop id of the current shop `connections.shopify.currentShopDomain` The myshopify domain of the the current shop `connections.shopify.currentClientId` The client ID of the Shopify app that the current shop installed `connections.shopify.currentClientSecret` The client secret of the Shopify app that the current shop installed `connections.shopify.currentSession.token` The Shopify session token that was used to authenticate the current shop for the current request `connections.shopify.currentSession.userId` The Shopify user id attached to the Shopify session token that was used to authenticate the current shop for the current request It is preferred to use `connections.shopify.current` in actions to prevent any cross-talk between shops. You can then use the `shopify-api-node` API off of the current shop to interact with the current Shopify store. ```typescript export const onSuccess: ActionOnSuccess = async ({ params, record, logger, api, connections, }) => { const shopify = connections.shopify.current; if (shopify) { await shopify.graphql( `mutation ($input: ProductInput!) { productUpdate(input: $input) { product { id tags } userErrors { message } } }`, { input: { id: `gid://shopify/Product/${record.id}`, tags: ["foo", "bar", "baz"], }, } ); } }; ``` It can also be useful to understand how [filtered model permissions](https://docs.gadget.dev/guides/access-control#filtered-model-permissions) work in Gadget, as they determine what default filters, such as `shopId` in the case of a public app, are applied to GraphQL queries and mutations made through your application's API. ### Current shop tenancy in HTTP routes If you are looking to get the current shop's context in a custom HTTP route being called from an app embedded in the Shopify admin, you can use the `useFetch` hook from `@gadgetinc/react` or the Gadget client's `api.fetch` method. You can then access the current shop id in your Gadget route. You may be able to use a Global Action instead of an HTTP route. One advantage to Global Actions is that they are included in a Gadget project's generated GraphQL API, so shop tenancy for an embedded app is handled automatically. For more differences between global actions and routes, see [global actions vs HTTP routes](https://docs.gadget.dev/guides/actions#global-actions-vs-http-routes). Here's an example of how we can do this in a React component from an embedded Shopify app: ```typescript // this is your Gadget project's API client import { api } from "../api"; import { useFetch } from "@gadgetinc/react"; export default function Example() { const [{ data, fetching, error }, send] = useFetch("/custom", { json: true }); return (
{JSON.stringify(data, null, 2)}