Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save airhorns/bd16f8b24424937e211e483a1475dd95 to your computer and use it in GitHub Desktop.
Save airhorns/bd16f8b24424937e211e483a1475dd95 to your computer and use it in GitHub Desktop.
Guide for upgrading a Shopify CLI generated app to set correct iframe security headers

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.

Why

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.

Migration steps

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.

Modifying production deployment config

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 to production, and SHOPIFY_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment