Building Shopify Apps: What the Docs Don't Tell You

March 01, 2024

Fighting a Hydra


I can’t count the times I’ve wrestled with CORS by now. It’s a hydra from which you cut off a head and another one grows in its place. But now I know how to handle this - it’s kerosene, a lighter, and knowing when to throw it.


Shopify App Extensions

The CORS Hydra

I do everything by the book, and make a correct request from an admin UI extension to the main app for some data from my backend, and it works one day, and stops working another day. I do some shuffling around, reauthorize my app with some magic, and get it working again. But it’s unreliable. Mostly though, I get CORS errors as Shopify seems to be unable to properly authorize the requests half of the time. You would expect using Shopify’s official Remix cors wrapper to work out of the box. It doesn’t.

When you tell your doctor “hey doc, I have a pain in my leg when I poke at it”, the tired wise doctor tells you to stop poking at it then. Similarly, I am tired. So my solution here is the same. Stupid, but works - I stop making requests from the extensions.

This approach has a couple of downsides though - for one it reveals too much inner workings of the code on the frontend if you let your business logic live there too much, and secondly I am still unable to connect to my own database like this, so workarounds need to be used.

I do really enjoy the separation of concerns though. Extension-related things are next to the extension itself.

UI Extension Components are the forgotten middle child version of Shopify UI Components

Even though the frontend UI components for the extension might be similar to the components available for the wrapper app, they are actually quite different, with less functionality in most cases, and more functionality in some. So it’s like working with a completely new component library with every different extension you are working with.

Function Extension: (a -> b) -> [a] -> [b]

With function extensions, any extra information you need for the proper business logic of the function, has to be supplied via metafields or metaobjects. The only realities for a discount function is its GraphQL query input and its strict output, with no network access.

Runtime: Boxed In and Hard to Use

The context in which extensions run are very limited, so there is no access to window or the DOM, including environment variables. You could add some environment variables, but these are not true environment variables, and process.env.[...] is replaced on compile time by Shopify. If you really want to use env vars here, then NODE_ENV seems to be available in at least some of the UI extensions. But generally, I’d avoid it.


Shopify provides a single hook 🪝 that is triggered on app install in the template’s shopify.server.ts file for which you can piggyback your own webhooks on, but I found it to be nearly unusable for development purposes. This is because you don’t reinstall your local app every time you get a new tunnel URL on npm run dev. So I wrote my own upsert code via GraphQL queries for these webhooks to run correctly. These also give me a higher degree of certainty things would also work in production if I have better control over them.


I’m glad to have simplified the process here a lot since the first time around.

Single Repository

Previously, I had two repositories for local and production, but this was mainly because Shopify’s documentation on how these systems interact is just so confusing. Having more experience with this now, I think I’ve figured out how to properly use a single repository for running the app for testing locally, but also for deploying it into production.

Insight 1: The .toml files are only for local use

So the template apps start off with in the root folder for running Shopify with its CLI, so you can test out things. The app ID there is also only for the test app itself, so this cannot be reused for production.

For production, Shopify suggests creating a new .toml file for testing purposes, however this is not loaded in any sense when you’re deploying to production. I have a file named, but this is meant to run production configuration from the CLI to test out things, something akin to staging. It also helps specify the wrapper app ID when you use this config to deploy your extensions.

So where is the configuration? It’s split between Shopify having some of the configuration in their own systems from what you write in the Partners -> App -> Configuration page; and another half of it coming from the environment variables that you have to find a way to load in when you deploy your application.

Insight 2: The extensions are deployed only via CLI

The app extensions that you have are hosted by Shopify themselves, and they mostly act like serverless functions and iframes with special privileges.

The way you deploy them is first choose the wrapper app that you are deploying the extensions into with npm run config:use and secondly run npm run deploy. I have been able to have separate deployments for local and production for the extensions using this method. I am still only mostly sure of this approach, but not completely.

Running Production

Docker Pixel Art

One of the biggest changes I have this time is ditching pm2 for docker. I noticed the npm build hogged all the memory on the machine, even failing as a result. So I went out searching for a solution where I could build the code on my machine first and then deploy it to the VM. Naturally, Docker was the simplest option here, mostly because of the ample documentation.

What I am now doing is building the app on my computer first, transmit this to my VM over SSH, and load the docker image and run this then and there. This has the added bonus of I don’t need to painstakingly pass all the environment variables to the pm2 command, but rather just use the corresponding parameter on docker run.

The most important line is this, where I set up the port for the app, the database, and on every deploy I am copying over the public files to the VM in order for me to serve them properly from apache. This can possibly be done easier, but it makes sense to me right now. The configuration for the Apache configuration is seen in one of the previous posts.

docker run -d -p <port-for-app>:<port-for-app-container> \
              -p <port-for-database>:<port-for-database-container> \
              -v <local-database-path>:<container-database-path> \
              --env-file=<path-to-env-file> \
              --name <name-of-app> <name-of-app>:<version-of-app> \
&& docker cp <name-of-app>:<public-folder-in-container> <destination-path-for-public-folder>


Of course, you can keep it simple and have the billing initially have isTest as true and later as false, but because I want to have a single codebase that functions based on the environment it is in, then this is the code I ended up with, with me supplying my own environment variables. I use the word 'yes' instead of 'true' because environment variables are all strings and that would confuse things. The code differs from the official documentation in some ways.

  // File: /app/routes/app.tsx
  if (process.env.REQUIRE_BILLING === 'yes') {
    const isTest = process.env.PRE_REVIEW === 'yes' || process.env.SHOPIFY_APP_ENV !== 'production';
    await billing.require({
        plans: [PLAN_STANDARD],
        isTest: isTest,
        onFailure: async () => await billing.request({ 
          plan: PLAN_STANDARD, 
          isTest: isTest 

I was a bit surprised that even though you could see multiple plans on the app listing, Shopify does not provide selector UI components for these for the customers to use. You have to build your own in order for you to offer multiple plans. The easiest is to start off with a single plan though, so you can just request that single plan whenever the customer attempts to use your app.

Authorization & Scopes Hell 401 401 401

Auth Hell Pixel Art

I wish I had any good takeaways here other than to keep it as simple as you possibly can with everything. Because when these 401s hit, you don’t know whether you’ve done something wrong, you’re missing a CORS header, you have something else wrong, you’re doing requests wrong, or you’re using wrong scopes. And often it’s just Shopify being unstable or handles cookies weirdly.

The best thing to do is to not make complicated requests and use GraphQL whenever possible in page loader functions and not as your own API endpoints.

Even though if this might have some ideal solution, then one thing’s for sure: the documentation for all of this is severely lacking. It’s just debug hell.

Words of Encouragement

If you’re not a seasoned engineer in this space and working on it too, then know that all of these aspects of trying to get an MVP out become simpler each time you do it. And also, if it was easy, everyone would do it, and you’d have nothing special to offer. So cheers to me and you that get through the painful bits and enjoy the moments thereafter.

Profile picture

This is the website of Silver Taza, that's me - nice to meet you!
My motto: Creating value ✨ with love ❤️ and beauty 🌹
Say hello to me on X/Twitter 🐦!

© 2024 Silver Taza