Shopify data security 

It is important to take the necessary steps to ensure that your Shopify app is secure and that sensitive data is protected. This guide will provide you with the necessary information to help you secure your Shopify app.

Built-in security measures 

All Gadget apps come with built-in security measures to help protect your application data, including a role-based authorization system. This system allows you to limit access to your app's data based on the role of the user making the request.

Shopify apps built with Gadget will have two default roles:

  • shopify-app-users: The role assigned to merchants interacting with your app in a store admin.
  • unauthenticated: All other users, including storefront shoppers.

It is important to make sure that each role has the appropriate and minimal permissions to access the data they need.

Gadget also has support for building multi-tenant, public Shopify apps, which ensures that merchants can only access data that belongs to their shop. By default, all Shopify models in Gadget are multi-tenant so that data is not inadvertently exposed to other merchants.

Storefront API access 

Shoppers in Shopify storefronts are unauthenticated users. The Storefront API provides unauthenticated access to Shopify data, with Shopify managing the authentication. This streamlined approach simplifies the process of making API requests with Shopify's Storefront API.

However, this presents a risk for data exposure if security practices and measures aren't taken into account when calling your Gadget API from the storefront.

Several measures have been built into Gadget to help enforce data security:

Browser-based API client creation restriction 

The system prevents the creation of API clients using API keys within web browsers. This precautionary step ensures that API keys are not inadvertently exposed. Consequently, any in-browser API clients will automatically receive the unauthenticated access role in Gadget.

Limited access for unauthenticated role 

The unauthenticated access role is intentionally restricted from accessing most Shopify models. This is a vital safeguard against inadvertent exposure of personally identifying information (PII) and other sensitive data.

Protected access tokens 

Access tokens stored on the shopifyShop model are intentionally omitted from the response when accessed through your Gadget app's public API. This measure prioritizes security and privacy. Nonetheless, these tokens can still be accessed via the internal API, should the need arise for server-side operations.

Securing your data: best practices 

Gadget's built-in security practices provide a sensible default that secures your application, but it's still your responsibility to ensure the security of each new API endpoint you add.

Audit API access 

The data in your Gadget database is typically sensitive data -- including Shopify merchants' data and especially customer personal information. Generally speaking, you should avoid granting read access to merchant data where possible, and if required for powering your user interface, you should only grant access to authenticated roles you know have permission to read the data.

Giving end users of your app access to your API should always be done consciously and with lots of scrutiny. Avoid unauthenticated access to sensitive model actions.

Changing access control permissions without considering the shape of returned data can give malicious users access to sensitive data. This is why we recommend that developers take the time to think through which model APIs that roles are given access to.

Use global actions for controlled data access 

When constructing global actions in Gadget, exercise caution to avoid disclosing sensitive data to unauthenticated users and retrieve only the requisite data needed. If you are building a multi-tenant Shopify app, ensure that the data being accessed belongs to the shop that is making the request. Read more about enforcing multi-tenancy in global actions.

Utilize the integrated roles and permissions system for authorization, ensuring controlled data access and enhancing overall data security within your application.

Safeguard HTTP Routes 

Authenticating access to HTTP routes must be done in your own route code -- Gadget doesn't protect HTTP routes by default. If you must serve sensitive data through routes, you can implement route protection using Gadget's authentication plugin.

Recommendation: use global actions

Because HTTP routes are unprotected by default, Gadget recommends using global actions when possible, instead of low-level HTTP routes.

Appropriately grant access to model read actions with non-sensitive data 

If a data model contains non-sensitive information, it's acceptable to grant unauthenticated users access to that model's API. Ensure that minimum permissions are granted, preventing shoppers from creating or updating records while allowing them to read data.

Use the Storefront API to fetch Shopify data 

Where possible, use the Shopify Storefront API for retrieving Shopify data. Shopify's Storefront API is performant and has out-of-the-box security settings that work well. Making requests to the Shopify Storefront API is done outside Gadget within a theme or theme extension, so Gadget's Shopify connection does not directly interface with it. This approach is suitable if you exclusively require data accessible through the Storefront API.

You can also use metafields and metaobjects to expose data from your backend to a storefront securely.

Adding multi-tenancy to custom models 

Shopify models managed by Gadget have multi-tenancy support built in. This means that merchants will only be able to read and write data that belongs to their shop and helps to enforce row-level security (RLS) for your Shopify app. This is a crucial security measure that ensures that data is not inadvertently exposed to other merchants.

When creating custom models for public Shopify apps, it is important to ensure that they are also multi-tenant.

Follow these 3 steps to add multi-tenancy to your custom models:

  1. Add a belongs to relationship to the shopifyShop model from your custom model. The inverse relationship should be a has many relationship from the shopifyShop model to your custom model.

For example, if you have a quiz model and want to relate it to the shopifyShop model, the relationship should be shopifyShop has many quiz.

A screenshot of a relationship between a quiz model and the shopifyShop model, from the shopifyShop's schema page. The relationship field is named quizzed, and is defined so that shopifyShop has many quiz records.
  1. Import the preventCrossShopDataAccess() function from gadget-server/shopify into all your model actions and call it in the run function before saving the record. This function ensures that the data being accessed belongs to the shop that is making the request.

preventCrossShopDataAccess must be called before the save function in your model actions!

For example, adding multi-tenancy to a quiz model action:

Multi-tenancy in api/models/quiz/actions/create.js
JavaScript
1import {
2 applyParams,
3 save,
4 ActionOptions,
5 CreateQuizActionContext,
6} from "gadget-server";
7import { preventCrossShopDataAccess } from "gadget-server/s";
8
9/**
10 * @param { CreateQuizActionContext } context
11 */
12export async function run({ params, record, logger, api, connections }) {
13 applyParams(params, record);
14 // Prevent merchants from modifying each others data
15 // must be called before save()
16 await preventCrossShopDataAccess(params, record);
17 await save(record);
18}
19
20// ... rest of action file

Each time preventCrossShopDataAccess runs, it will make sure the given record has the correct shopId for the shop processing this action. For new records, that means it will assign the shopId, and for existing records, it will verify that it matches.

  1. Add a tenancy filter to your model in accessControl/permissions. Tenancy filters in Gadget are written in Gelly, Gadget's data access language.

Most tenancy filters will check to make sure the shopId of the current session is equal to the shopId stored on the current model, and will look like this:

A sample filter on a quiz model
gelly
filter ($session: Session) on Quiz [
where shopId == $session.shopId
]
Where does shopId come from?

The shopId field is generated when you add the belongs to relationship to the shopifyShop model. The name of this field is the name of the relationship followed by Id.

For example, if the relationship is named shop, the field will be named shopId.
If it is named cowboy, the field will be named cowboyId.

We don't advise naming your relationships to the shopifyShop model cowboy.

After following these steps, your models are set up for multi-tenancy.

Refer to use global actions for controlled data access for more information on how to control data access in HTTP routes and global actions.

Multi-tenancy in global actions 

When building multi-tenant applications such as Public Shopify apps, ensure that the data being accessed belongs to the shop that is making the request. This typically means that you should be filtering data based on the shopId of the shop making the request.

For example:

Filtering by shopId in api/globalActions/getCustomSettings.js
JavaScript
1import { GetCustomSettingsGlobalActionContext } from "gadget-server";
2
3/**
4 * @param { GetCustomSettingsGlobalActionContext } context
5 */
6export async function run({ params, logger, api, scope, connections }) {
7 // get the current shop id
8 const shopId = connections.shopify.currentShopId;
9 // get the data for the current shop
10 const settings = await api.customSettings.findMany({
11 filter: {
12 shopId: {
13 equals: shopId,
14 },
15 },
16 });
17
18 // do additional processing, if required
19
20 return settings;
21}

It is important to test your multi-tenancy implementation thoroughly to ensure that data is not inadvertently exposed to other merchants.