This document explains how to update a Shopify CLI generated application from the setup Gadget previously recommended before Dec 21, 2022, to the new setup Gadget recommends.
Previously, Gadget recommended removing the web/package.json
and web/shopify.web.toml
files, stripping away the node.js server that served the frontend application in web/frontend
. Most web hosts have functionality to serve static files in the same manner this node.js application does, as well as set up redirects and headers. Since Gadget handles the majority of the backend, this node.js backend largely duplicates Gadget functionality.
That said, it is annoying to set up each different frontend hosting provider's tooling to meet Shopify's strict iframe
security requirements for apps destined for the Public App Store. Shopify requires that each application is served to the embedded app iframe with the Content-Security-Policy
header set to a secure value. Read more about Shopify's iframe security requirements here. Instead of setting up bespoke, error-prone custom edge middleware on each deployment platform, Gadget now recommends keeping a lightweight nodejs process within the generated CLI application structure that serves static files and sets this important header.
We need to re-create the nodejs program in the web/
folder in the CLI template. Add the following files back to your project (these are the default contents from a freshly generated CLI application):
in web/package.json
:
{
"name": "shopify-app-template-node",
"private": true,
"license": "UNLICENSED",
"scripts": {
"debug": "node --inspect-brk index.js",
"dev": "cross-env NODE_ENV=development nodemon index.js --ignore ./frontend",
"serve": "cross-env NODE_ENV=production node index.js"
},
"type": "module",
"engines": {
"node": ">=14.13.1"
},
"dependencies": {
"@shopify/shopify-app-express": "^1.0.0",
"@shopify/shopify-app-session-storage-sqlite": "^1.0.0",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"local-ssl-proxy": "^1.3.0",
"serve-static": "^1.14.1"
},
"devDependencies": {
"jsonwebtoken": "^8.5.1",
"nodemon": "^2.0.15",
"prettier": "^2.6.2",
"pretty-quick": "^3.1.3"
}
}
After adding this package.json to the web/
folder, we need to install these dependencies:
cd web && npm install
Then, we need to create web/shopify.web.toml
with these contents:
type="backend"
[commands]
dev = "npm run dev"
And then we need to create web/index.js
with following code to serve the application in production with the correct security header:
// @ts-check
import { join } from "path";
import * as fs from "fs";
import express from "express";
import serveStatic from "serve-static";
const __dirname = new URL('.', import.meta.url).pathname;
const PORT = parseInt(process.env.BACKEND_PORT || process.env.PORT, 10);
const STATIC_PATH = process.env.NODE_ENV === "production" ? `${__dirname}/frontend/dist` : `${__dirname}/frontend/`;
const app = express();
// return Shopify's required iframe embedding headers for all requests
app.use((req, res, next) => {
const shop = req.query.shop;
if (shop) {
res.setHeader(
"Content-Security-Policy",
`frame-ancestors https://${shop} https://admin.shopify.com;`
);
}
next();
});
// serve any static assets built by vite in the frontend folder
app.use(serveStatic(STATIC_PATH, { index: false }));
// serve the client side app for all routes, allowing it to pick which page to render
app.use("/*", async (_req, res, _next) => {
return res
.status(200)
.set("Content-Type", "text/html")
.send(fs.readFileSync(join(STATIC_PATH, "index.html")));
});
app.listen(PORT);
And then we need to restore the default vite configuration for the web/frontend
package that will proxy requests to this backend.
Replace the web/frontend/vite.config.js
contents with this code:
import { defineConfig } from "vite";
import { dirname } from "path";
import { fileURLToPath } from "url";
import react from "@vitejs/plugin-react";
if (
process.env.npm_lifecycle_event === "build" &&
!process.env.CI &&
!process.env.SHOPIFY_API_KEY
) {
console.warn(
"\nBuilding the frontend app without an API key. The frontend build will not run without an API key. Set the SHOPIFY_API_KEY environment variable when running the build command.\n"
);
}
const proxyOptions = {
target: `http://127.0.0.1:${process.env.BACKEND_PORT}`,
changeOrigin: false,
secure: true,
ws: false,
};
const hmrConfig = {
protocol: "ws",
host: "localhost",
port: 64999,
clientPort: 64999,
};
}
export default defineConfig({
root: dirname(fileURLToPath(import.meta.url)),
plugins: [react()],
define: {
"process.env": JSON.stringify({
SHOPIFY_API_KEY: process.env.SHOPIFY_API_KEY,
NODE_ENV: process.env.NODE_ENV,
}),
},
resolve: {
preserveSymlinks: true,
},
server: {
host: "localhost",
port: process.env.FRONTEND_PORT,
hmr: hmrConfig,
proxy: {
"^/(\\?.*)?$": proxyOptions,
"^/api(/|(\\?.*)?$)": proxyOptions,
},
},
});
With the following in place, running a server in production with node index.js
will serve the vite-based frontend with the correct security headers.
If deploying your application to Vercel, the production deployment configuration must change to now run the index.js
serverless function, instead of just serving the static files as a vite app. To update vercel to run this serverless function add the following to web/vercel.json
{
"version": 2,
"outputDirectory": ".",
"builds": [
{
"src": "index.js",
"use": "@vercel/node",
"config": {
"includeFiles": [
"./frontend/dist/**"
]
}
}
],
"routes": [{ "src": "/(.*)", "dest": "/" }]
}
You must also add the following build script to the web/package.json
:
// in web/package.json
// under `scripts:` add this build script:
{
//...
"scripts": {
"vercel-build": "cd frontend && npm install && npm run build"
}
}
This build script will install the Shopify CLI dependencies and run the vite build when vercel builds this application.
You must also set some settings in the Vercel settings area:
- the root folder of your application to the
web
folder: https://share.cleanshot.com/ckWQvZFr - the two normally required environemnt variables in the Vercel Environment Variables configuration must be set,
NODE_ENV
set toproduction
, andSHOPIFY_API_KEY
set to your production Shopify app's API key from the Partners dashboard: https://share.cleanshot.com/98t4WjRf
This configuration sets up Vercel to run the index.js
serverless function in production, and to build the vite app for it to serve when deploying.