├── .eslintrc.cjs ├── .example.vars ├── .gitignore ├── .node-version ├── README.md ├── app ├── components │ └── LiveReload.tsx ├── db.server.ts ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx ├── routes │ ├── _index.tsx │ ├── auth.$.tsx │ └── webhooks │ │ ├── carts │ │ └── create.tsx │ │ ├── config.tsx │ │ └── route.tsx └── shopify.server.js ├── drizzle.config.ts ├── example.wrangler.toml ├── migrations ├── 0000_bumpy_stick.sql └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── package.json ├── pnpm-lock.yaml ├── public ├── _headers ├── _routes.json └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── schema.ts ├── server.ts ├── shopify.app.toml ├── shopify.web.toml ├── tsconfig.json └── worker-configuration.d.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], 4 | }; 5 | -------------------------------------------------------------------------------- /.example.vars: -------------------------------------------------------------------------------- 1 | SHOPIFY_APP_KEY="CLIENT KEY" 2 | SHOPIFY_APP_SECRET="SECRET KEY" 3 | APP_URL="YOUR APPS URL" 4 | SHOPIFY_APP_SCOPES="read_products,write_products,read_script_tags,write_script_tags" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /functions/\[\[path\]\].js 5 | /functions/\[\[path\]\].js.map 6 | /functions/metafile.* 7 | /functions/version.txt 8 | /public/build 9 | .dev.vars 10 | 11 | wrangler.toml 12 | priv.shopify.app.toml 13 | shopify.app.ignore.toml 14 | .wrangler 15 | .vscode/database-client-config.json 16 | worker-configuration.d.ts 17 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.0.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A template for developing Shopify Apps using RemixJS and Cloudflare Workers 2 | 3 | ## Why? 4 | I'm a glutton for performance and I love the idea of using Cloudflare Workers to serve my Shopify App from their global CDN. I also really enjoy working with RemixJS and especially with Shopify apps. 5 | 6 | This template is a starting point for building a Shopify App using RemixJS and Cloudflare Workers. It's not a complete app, but it does provide a good starting point for building a Shopify App and I will be continuing to implement Cloudflare technologies into it. (Like KV, D1, etc.) 7 | 8 | ## Getting Started 9 | 10 | ### 1. Clone this repo 11 | ```bash 12 | git clone git@github.com:refactor-this/Shopify-RemixJS-Cloudflare-Workers-Template.git 13 | ``` 14 | ### 2. Install dependencies 15 | I'm using pnpm to manage dependencies. You can install it with npm or yarn if you prefer. 16 | ```bash 17 | pnpm install 18 | ``` 19 | 20 | ### 3. Create your environment variables 21 | Copy the `example.wrangler.toml` to your own `wrangler.toml` file and fill in the environment variables. (You should have a Shopify app created already on their partner dashboard so you can get the client id and secret.) 22 | 23 | For the app url, I set up a free tunnel service using Cloudflare. You can follow how I set that up here: https://innovonics.com/creating-a-free-tunnel-service-for-developing-shopify-apps/ 24 | 25 | As an alternative, you can use http://localhost:8002 26 | 27 | You will also need to copy the `.example.vars` to `.dev.vars` and fill out the required values. 28 | 29 | The reason we use the local `.{environment}.vars` file is because we want to keep the sensitive information out of the `wrangler.toml` file and out of git history. 30 | 31 | When adding the productions values, you would add them using the Cloudflare CLI with the encrypted flag set to `true`. This would keep the values secret but have no effect on the worker. 32 | 33 | ### 4. Start the dev server 34 | ```bash 35 | pnpm dev 36 | ``` 37 | ### 5. Happy coding! 38 | 39 | ## Creating the D1 Database 40 | 41 | Creating the D1 database is fairly straightforward, run the next command to create one. 42 | 43 | ```bash 44 | wrangler d1 create d1-example 45 | ``` 46 | 47 | You will receive an output in the terminal that looks like this: 48 | ```bash 49 | [[d1_databases]] 50 | binding = "DB" # i.e. available in your Worker on env.DB 51 | database_name = "your-database-name" 52 | database_id = "your-generated-database-id" 53 | ``` 54 | 55 | copy and paste it to your wrangler.toml file. 56 | 57 | 58 | Now that we have our DB created, let's generate and apply migrations: 59 | 60 | Generate migrations 61 | ```bash 62 | pnpm db:generate 63 | ``` 64 | Apply migrations 65 | ```bash 66 | pnpm dev:db:apply 67 | ``` 68 | 69 | You can also list pending migrations with 70 | ```bash 71 | pnpm dev:db:list 72 | ``` 73 | 74 | ### Viewing data in your current database 75 | 76 | You can view the data in your current database by running the following command: 77 | ```bash 78 | pnpm db:studio:preview 79 | ``` 80 | This will open a Drizzle preview connection which you can view on your browser. 81 | 82 | Or if you are like me an use a 3rd party tool you can access the D1 SQLite database directly. It is located at the top of your project folder. 83 | ```bash 84 | .wrangler/state/v3/d1/miniflare-D1DatabaseObject/[some-random-string].sqlite 85 | ``` 86 | 87 | ## Webhooks 88 | The webhooks file is set up with the standard `app/uninstall` solution (delete the session in your database). 89 | 90 | ~~It continues with the stadard switch/case solution. This is not necessarily how I would handle it when the application grows and will be subject to change.~~ 91 | 92 | ~~It would make more sense to me to use the routing to define webhook handling and set up a config file when registering webhooks.~~ 93 | 94 | I ended up just implementing both for you to choose which you like. 95 | 96 | #### Route-based webhooks 97 | 98 | In `~/routes/webhooks/carts/create.tsx` You can see how I would handle the route-based approach. The webhook logic is defined by the route and used as the endpoint shown in `~/routes/webhooks/config.tsx` which is used as a global webhook endpoint definition file. 99 | 100 | #### Standard Switch/Case webhooks 101 | 102 | You can view this method in `~/routes/webhooks/route.tsx`. This is the standard switch/case solution. It uses the `topic` to determine the logic to handle the webhook. It works for small solutions but personally the route-based approach is a long term optimiation that makes more sense. 103 | 104 | -------------------------------------------------------------------------------- /app/components/LiveReload.tsx: -------------------------------------------------------------------------------- 1 | export const LiveReload = 2 | process.env.NODE_ENV !== "development" 3 | ? () => null 4 | : function LiveReload({ 5 | port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002), 6 | }: { 7 | port?: number; 8 | }) { 9 | let setupLiveReload = ((port: number) => { 10 | let protocol = location.protocol === "https:" ? "wss:" : "ws:"; 11 | let host = location.hostname; 12 | let socketPath = `${protocol}//${host}:${port}/socket`; 13 | 14 | let ws = new WebSocket(socketPath); 15 | ws.onmessage = (message) => { 16 | let event = JSON.parse(message.data); 17 | if (event.type === "LOG") { 18 | console.log(event.message); 19 | } 20 | if (event.type === "RELOAD") { 21 | console.log("💿 Reloading window ..."); 22 | window.location.reload(); 23 | } 24 | }; 25 | ws.onerror = (error) => { 26 | console.log("Remix dev asset server web socket error:"); 27 | console.error(error); 28 | }; 29 | }).toString(); 30 | 31 | return ( 32 |