Shopify app frontends
Gadget provides a rich set of tools for quickly building frontends for Shopify Apps. Each Gadget app connected to Shopify gets a basic, hosted frontend powered by Vite out of the box that already supports OAuth, Shopify's Polaris design system, multi-tenant data security, and Shopify's App Bridge for embedded applications. You can customize this frontend to your liking, or you can build your own external frontend from scratch using Gadget's React packages.
Frontends for Shopify apps communicate with Gadget backends using your Gadget app's GraphQL API and the associated JS client for your application. There are a few different npm packages necessary for working with Gadget from a Shopify app frontend:
Package | Description | Available from |
---|---|---|
@shopify/app-bridge | Shopify's React package for embedding React applications within the Shopify Admin | npm |
@gadget-client/example-app | The JS client for your specific Gadget application | Gadget NPM registry |
@gadgetinc/react | The Gadget React bindings library, providing React hooks for making API calls | npm |
@gadgetinc/react-shopify-app-bridge | The Gadget Shopify wrapper library for Shopify Embedded App setup and authentication | npm |
Gadget installs these packages into your Gadget-hosted frontend automatically, but if you're building an external frontend you must install them yourself.
Building Shopify apps
Once you've set up your Shopify Connection, your Gadget app will have a built-in frontend ready for construction in your app's frontend
folder. This frontend can access data from your backend, including both models synced from Shopify and models created within your application. By default, your app uses React and Shopify's standard @shopify/app-bridge-react
library, so Shopify's normal helpers for navigation and data fetching are ready for use as well.
Gadget frontends for Shopify include Shopify's design system Polaris via the @shopify/polaris
package as well, so your app is compliant with Shopify's App Store guidelines out of the box.
If you'd like to build your frontend outside of Gadget, refer to the External Frontends guide.
Reading data from your backend
Data from your Gadget application, including the non-rate-limited copies of Shopify models and your own created models, can be accessed easily from the frontend using the @gadgetinc/react
hooks library. This library provides hooks like useFindOne
, useFindMany
, useFindFirst
for fetching data from your Gadget app.
Each of these hooks returns an object with the requested data
, the fetching
state, and an error
if one was encountered, as well as a refetch
function for refreshing the data if need be.
For example, if you have the Shopify Product model enabled in your Connection, we can fetch product records in a variety of ways:
jsx1// fetch one product by id2const [{ data, fetching, error }, refetch] = useFindOne(api.shopifyProduct, 10);34// fetch the first 10 products5const [{ data, fetching, error }, refetch] = useFindMany(api.shopifyProduct, { first: 10 });67// fetch the first product with the title field equal to "Socks", throw if it isn't found8const [{ data, fetching, error }, refetch] = useFindFirst(api.shopifyProduct, { where: { title: "Socks" } });910// fetch the first product with the title field equal to "Socks", return null if it isn't found11const [{ data, fetching, error }, refetch] = useMaybeFindFirst(api.shopifyProduct, { where: { title: "Socks" } });
Data from other models that you've created in your application is accessed the same way. For example, if we're building a free shipping banner app, you might create a Banner model that stores details about each shipping banner created. We can fetch banner records with the same React hooks:
jsx// fetch one banner by idconst [{ data, fetching, error }, refetch] = useFindOne(api.banner, 10);// fetch the first 10 bannersconst [{ data, fetching, error }, refetch] = useFindMany(api.banner, { first: 10 });
Each of these hooks must be wrapped in a React component to render. For example, we can use useFindMany
to display a list of products in a component:
jsx1import { useFindMany } from "@gadgetinc/react";2import { api } from "../api";34export const ProductsList = (props) => {5 const [{ data, fetching, error }, _refetch] = useFindMany(api.shopifyProduct, { first: 10 });67 if (fetching) {8 return <div>Loading...</div>;9 }1011 if (error) {12 return <div>Error: {error.message}</div>;13 }1415 return <ul>16 {data.map((product) => <li key={product.id}>{product.title}</li>)}17 </li>18}
For more on reading data in the frontend, see the Building Frontends guide and the@gadgetinc/react Readme.
Shopify permissions
By default, Gadget implements multi-tenant data permissions for your application. An app loaded for one Shopify shop is only able to access data from within that shop, and is not permitted to access data for other shops. This is the default setup that uses the Shopify App User role, and can be customized by editing the permissions for this role on the Roles & Permissions page.
Gadget enforces multi-tenant data permissions for the Shopify models by default, and does not grant automatic access to your own models. To grant the frontend access to your models, add permissions to the Shopify App User role for each model you'd like to access. For example, if you'd like to access a model named Banner, check the Read
permission checkbox in the Roles & Permissions screen.
For more information, see the Access Control guide.
Writing data back to your backend
Shopify app frontends can use the useAction
and useGlobalAction
hooks from the @gadgetinc/react
hooks library to write data back to your database.
To update data within Shopify, you must make an API call to Shopify directly from your backend. See the Calling Shopify API section for more information.
To write data back to your database for models you've created, or fields you've added to Shopify Models, use the useAction
or useGlobalAction
hook in a React component.
For example, if we create a new model within our app called Banner, we can use the useAction
hook to create a new banner record:
jsx1import { useAction } from "@gadgetinc/react";2import { api } from "../api";34export const CreateBannerForm = (props) => {5 const [message, setMessage] = useState("");6 const [{ data, fetching, error }, act] = useAction(api.banner.create);78 if (fetching) {9 return <div>Saving...</div>;10 }1112 if (error) {13 return <div>Error: {error.message}</div>;14 }1516 return (17 <form18 onSubmit={() => {19 // run the action function when the form is submitted20 // the component will re-render with `fetching: true` initially, and then when the response arrives, render again with the result in `data`.21 void act({ message });22 }}23 >24 <label>Message</label>25 <textarea onChange={(e) => setMessage(e.target.value)}>{message}</textarea>26 <input type="submit" />27 </form>28 );29};
For more details on the useAction
hook, see the @gadgetinc/react
Readme, and see the Building Frontends guide for more examples.
Calling Global Actions
Shopify app frontends can call your backend's Global Actions with the useGlobalAction
hook. See the Building Frontends guide for more information.
Calling HTTP routes
Shopify app frontends can call your backend's HTTP Routes with api.fetch
, or any other HTTP client for React. See the Building Frontends guide for more information.
If your HTTP routes require authentication, or need to access the connections
object server side, you must ensure you pass the correct
authentication headers to your HTTP route from your frontend. You can do this automatically by using api.fetch
instead of the built-in
browser fetch
.
For more information, see the Building Frontends guide.
Calling the Shopify API
Gadget generally recommends reading data through Gadget, as Gadget's API doesn't restrict access to data in the same way that Shopify's API does. Your app's Gadget API is not rate-limited, so you don't have to carefully manage requests like you might with Shopify. However, if you need to access data that your app doesn't sync, or if you need to access data that Gadget doesn't have access to, you can use Shopify's API directly.
Shopify supports making requests to the Admin API from the backend of your API best. So, to call Shopify, you make a call from your frontend application to your Gadget backend, and then your Gadget backend calls the Shopify API, and returns results to your frontend.
There's two easy ways to set this up in Gadget: Actions if you want to write data, or HTTP routes if you just want to read data.
Calling Shopify within Actions
Effect code within Actions or Global Actions can make API calls to Shopify using the connections.shopify.current
Shopify API client Gadget provides.
For example, if we want to create a product from the frontend of our Gadget application, we can create an Global Action that calls the Shopify API:
First, we create a new Global Action called createProduct
within the Global Actions section of the Gadget editor.

Then, we can add the following code to the Global Action's effect:
global/createProduct/onCreateProduct.jsJavaScript1module.exports = async ({ scope, logger, params, connections }) => {2 const shopify = connections.shopify.current;3 const product = await shopify.product.create({4 title: "test 123",5 });6 logger.info({ product }, "created new product in shopify");7 scope.result = product;8};
This effect uses Gadget's connections
object to access the Shopify API client, and then calls the product.create
method to create a new product in Shopify.
We can then call this global action from our Shopify frontend with the useGlobalAction
React hook. For example, we can call this action when a button is clicked in the app:
frontend/components/CreateProductButton.jsjsx1import { useGlobalAction } from "@gadgetinc/react";2import { api } from "../api";34export const CreateProductButton = (props) => {5 const [{ data, fetching, error }, act] = useGlobalAction(api.createProduct);67 return (8 <button disabled={fetching} onClick={() => void act()}>9 Create Product10 </button>11 );12};
Gadget's synced Shopify models are one-way synced out of Shopify. You can't call the api.shopifyProduct.create
action from the frontend
to create a product, as that would put your Gadget app out of sync with Shopify. You must use the Shopify API to create resources that
Shopify owns.
Calling Shopify in HTTP Routes
Route code within HTTP Routes can access the Shopify API using request.connections.shopify
to create a Shopify API client object, similarly to Actions or Global Actions.
For example, an HTTP route could return an up-to-date list of products from the Shopify API:
JavaScript1module.exports = async (request, reply) => {2 const shopify = request.connections.shopify.current;3 const products = await shopify.product.list();4 reply.send({5 products,6 });7};
If you're accessing the connections
object server side, you must ensure you pass the correct authentication headers to your HTTP route
from your frontend. You can do this automatically by using api.fetch
in the browser instead of the built-in browser fetch
.
For more information, see the Building Frontends guide.
Standalone Shopify Apps
With a standalone Shopify app, you'll need to handle authentication and tenancy yourself as there is no Shopify session token to derive this from.
If your Shopify app is not an embedded app, you can still use the Gadget frontends to build your app. To do so, you will need to set the type
prop on GadgetProvider
to AppType.Standalone
.
<GadgetProvider type={AppType.Standalone} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api} router={appBridgeRouter}><AuthenticatedApp /></GadgetProvider>
Next is to update your routes/shopify/GET-install.js
route file to redirect users back to your frontend instead of the Shopify admin.
1// replace the following2// if (embedded) {3// return await reply.redirect("/?" + new URLSearchParams(query).toString());4// } else {5// const host = Buffer.from(base64Host, 'base64').toString('ascii');6// return await reply.redirect(`https://${host}/apps/${apiKey}`);7// }89return await reply.redirect("/");
This will ensure that once a shop install has completed users will be redirected to your frontend.
Shopify frontend security requirements
Shopify requires app developers to meet a set of important security requirements when building embedded apps. Gadget meets each security requirement out of the box:
- Gadget apps are always served over HTTPS
- Gadget apps run behind a production-grade firewall and only expose the necessary services
- Gadget apps use robust, multi-tenant authentication that limits access to data to the current shop (see the Access Control guide)
- Gadget sets up GDPR webhook listeners automatically for Shopify apps (see the GDPR docs)
- Gadget uses Shopfiy's Session Token authentication mechanism for authenticating the frontend to the backend.
- Gadget automatically sets the
Content-Security-Policy
header necessary for Shopify's IFrame protection when serving Shopify apps (see Shopify's security docs)
If you have any questions about the security posture of Gadget applications for Shopify, please join us in our Discord to discuss!
Reference
The Provider
The @gadgetinc/react-shopify-app-bridge
library handles authentication of your embedded app via the Provider
component. This provider has two main benefits - it handles authentication and the series of redirects required to complete an embedded app OAuth flow in Shopify, and it handles retrieving a Shopify session token from the App Bridge and passing it along to Gadget for authenticated calls.
The Provider handles these key tasks automatically:
- Starts the OAuth process with new users of the application using Gadget, escaping Shopify's iframe if necessary
- Establishes an iframe-safe secure session with the Gadget backend using Shopify's Session Token authentication scheme
- Sets up the correct React context for making backend calls to Gadget using
@gadgetinc/react
The Provider
has the following required props:
export interface ProviderProps {type: AppType; // 'AppType.Embedded' or 'AppType.Standalone'shopifyApiKey: string; // the API key from your Shopify app in the partner dashboard that is used with the Shopify App Bridgeapi: string; // the API client created using your Gadget application}
The Gadget provider will handle detecting if your app is being rendered in an embedded context and redirect the user through Shopify's OAuth flow if necessary.
The useGadget
React hook
The Provider
handles initializing the App Bridge for us. Now we can build our application component and use the initialized instance of App Bridge via the appBridge
key returned from the embedded React hook useGadget
.
useGadget
provides the following properties:
1export interface useGadget {2 isAuthenticated: boolean; // 'true' if the user has completed a successful OAuth flow3 isEmbedded: boolean; // 'true' if the app is running in an embedded context4 isRootFrameRequest: boolean; // 'true' if a user is viewing a "type: AppType.Embedded" app in a non-embedded context, for example, accessing the app at a hosted Vercel domain5 loading: boolean; // 'true' if the OAuth flow is in process6 appBridge: AppBridge; // a ready-to-use app bridge from Shopify, you can also use the traditional useAppBridge hook in your components to retrieve it.7}
useGadget
example
The following example renders a ProductManager component that makes use of Shopify App Bridge components and is ready to be embedded in a Shopify Admin page.
1import { useAction, useFindMany } from "@gadgetinc/react";2import { useGadget } from "@gadgetinc/react-shopify-app-bridge";3import { Button, Redirect, TitleBar } from "@shopify/app-bridge/actions";4import { api } from "./api.ts";56function ProductManager() {7 const { loading, appBridge, isRootFrameRequest } = useGadget();8 const [_, deleteProduct] = useAction(api.shopifyProduct.delete);9 const [{ data, fetching, error }, refresh] = useFindMany(api.shopifyProduct);1011 if (error) return <>Error: {error.toString()}</>;12 if (fetching) return <>Fetching...</>;13 if (!data) return <>No widgets found</>;1415 // Set up a title bar for the embedded app16 const breadcrumb = Button.create(appBridge, { label: "My breadcrumb" });17 breadcrumb.subscribe(Button.Action.CLICK, () => {18 appBridge.dispatch(Redirect.toApp({ path: "/breadcrumb-link" }));19 });2021 const titleBarOptions = {22 title: "My page title",23 breadcrumbs: breadcrumb,24 };25 TitleBar.create(appBridge, titleBarOptions);2627 return (28 <>29 {loading && <span>Loading...</span>}30 {isRootFrameRequest && (31 <span>App can only be viewed in the Shopify Admin!</span>32 )}33 {!loading &&34 !isRootFrameRequest &&35 data.map((widget, i) => (36 <button37 key={i}38 onClick={(event) => {39 event.preventDefault();40 void deleteProduct({ id: widget.id }).then(() => refresh());41 }}42 >43 Delete {widget.title}44 </button>45 ))}46 </>47 );48}
GraphQL queries
When building embedded Shopify apps, there may be instances where a Shop's installed scopes have not been updated to match the required scopes in your Gadget app's connection. In these situations, it is necessary to re-authenticate with Shopify so that the app can acquire the updated scopes. The following GraphQL query can be run using the app client (passed to the Provider
) and provides information related to missing scopes and whether a re-authentication is necessary.
1query {2 shopifyConnection {3 requiresReauthentication4 missingScopes5 }6}
Session model management
The Shopify Connection in Gadget automatically manages records of the backend Session model when using @gadgetinc/react-shopify-app-bridge
. When a merchant first loads up the frontend application, the <Provider/>
will retrieve a Shopify Session Token from Shopify's API, and pass it to your Gadget backend application. The Gadget Shopify Connection will then validate this token. If valid, the connection will provision a new record of the Session model with the correct shopId
field set up. This session is then passed to all your backend application's model filters and is available within Action code snippets.
Embedded app examples
Want to see an example of an embedded Shopify app built using Gadget?
Check out some of our example apps on GitHub, including: