Skip to content

Shopify React Router (v7) app running on Cloudflare

Notifications You must be signed in to change notification settings

chr33s/shopflare

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

611 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Note

v2 branch tracks Polaris React changes

ShopFlare

Minimalist Shopify app using React Router (v7) running on cloudflare (worker, kv, queues). Only essential features, no future changes other than core upgrades & platform alignment.

Rationale

  • Needed a simple starter, than focusses on the basics (optional extensions shoputils)
  • @shopify/shopify-[api,app-remix/react-router] was to complex due to platform abstractions
  • Wanted small code surface, easier audit, that focussed on stability over features
  • Modular, extendable, tree shakable (remove factory functions) -> smaller bundle size
  • Minimally opinionated, by supporting only:
    1. Embedded app use-case
    2. New Embedded Auth Strategy
    3. Managed Pricing
  • Optimized for Cloudflare stack
  • Tested - (unit, browser, e2e)

Assumptions

Familiarity with React, ReactRouter, Cloudflare, Shopify conventions.

Requirements

  1. Cloudflare account
  2. Shopify Partner account
  3. Node.js & NPM see package.json#engines brew install node@22
  4. cloudflared cli brew install cloudflared (optional)
  5. Github cli brew install gh (optional)

Note

  • For wss:// to work on a cloudflare tunnel, you need to set "Additional application settings" > "HTTP Settings" > "HTTP Host Header" to match the service URL (e.g. 127.0.0.1), otherwise the tunnel returns a 502 http status & client connection fails
  • To bypass caching set: Caching > Cache Rules ["Rule Name" = "bypass cache on tunnel", "Custom filter expression" = "", Field = Hostname, Operator = equals, Value = tunnel url, "Cache eligibility" = "Bypass cache", "Browser TTL" = "Bypass cache" ]

Setup

npm install
cp .env.example .env                                    # update vars to match your env values from partners.shopify.com (Apps > All Apps > Create App)
# vi [wrangler.json, shopify.app.toml]                  # update vars[SHOPIFY_API_KEY, SHOPIFY_APP_URL], SHOPIFY_APP_URL is the cloudflare tunnel url (e.g. https://shopflare.trycloudflare.com) in development and the cloudflare worker url (e.g. https://shopflare.workers.dev) in other environments.
npx wrangler secret put SHOPIFY_API_SECRET_KEY
npx wrangler kv namespace create shopflare              # update wranglers.json#kv_namespaces[0].id
gh secret set --app=actions CLOUDFLARE_API_TOKEN        # value from dash.cloudflare.com (Manage Account > Account API Tokens > Create Token)
gh secret set --app=actions SHOPIFY_CLI_PARTNERS_TOKEN  # value from partners.shopify.com (Settings > CLI Token > Manage Tokens > Generate Token)
gh variable set SHOPIFY_API_KEY

Development

# vi .env               # update vars[SHOPIFY_APP_LOG_LEVEL] sets logging verbosity.
npm run deploy:shopify  # only required on setup or config changes
npm run gen
npm run dev             # or npm run dev:shopify:tunnel
# open -a Safari ${SHOPIFY_APP_URL}

Production

npm run build
npm run deploy

To split environments see Cloudflare and Shopify docs.

Documentation

Usage

import * as shopify from "~/shopify.server";

export async function loader({ request }) {
  return shopify.handler(async () => {
    const { client } = await shopify.admin(request); // shopify[admin|proxy|webhook](request);
    const { data, errors } = await client.request(/* GraphQL */ `
      query {
        shop {
          name
        }
      }
    `);

    shopify.config();
    await shopify.client({ accessToken, shop }).admin(); // [admin | storefront](headers?)
    await shopify.redirect(request, { shop, url });
    await shopify.session("admin").get(sessionId); // set(id, value | null);
    shopify.utils.addCorsHeaders(request, responseHeaders);

    await shopify.bulkOperation(client).query(); // .mutation(mutation, variables);
    await shopify.metafield(client).get(identifier); // .set(identifier, metafield || null);
    await shopify.metaobject(client).get({ handle }); // .set({handle}, metaobject || null);
    await shopify.upload(client).process(file); // .[stage,target](file)

    return { data, errors };
  });
}

// Alternative (Backwards compatible)

import { createShopify } from "~/shopify.server";

export async function loader({ request }) {
  const shopify = createShopify();
  const client = await shopify.admin(request);
  const { data, errors } = await client.request(/* GraphQL */ `
    query {
      shop {
        name
      }
    }
  `);

  shopify.config;
  await shopify.redirect(request, { shop, url });
  await shopify.session.get(sessionId); // set(id, value | null);
  shopify.utils.addCorsHeaders(request, responseHeaders);

  const adminClient = createShopifyClient({
    accessToken,
    headers: { "X-Shopify-Access-Token": "?" },
    shop,
  });
  const storefrontClient = createShopifyClient({
    accessToken,
    headers: { "X-Shopify-Storefront-Access-Token": "?" },
    shop,
  });
}

Experimental > DurableObject

/// Direct api access

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (/[/]shopify[/](admin|storefront)([/])?/.test(url.pathname)) {
      const shop = url.searchParams.get('shop');
      const stub = env.SHOPIFY_DURABLE_OBJECT.getByName(shop);
      return stub.fetch(request);
    }
    // ... handler
  },
}

function Component() {
  useEffect(() => {
    fetch('/shopify/admin', { // /shopify/storefront
      body: JSON.stringify({
        query: /* GraphQL */ `
          #graphql
          query Shop {
            shop {
              name
            }
          }
        `,
        variables: {},
      }),
      credentials: 'include'
      method: 'POST',
    })
      .then<{data: ShopQuery}>((res) => res.json())
      // ...
  }, []);

  // .... jsx
}

export async function action({request}) {
  const shop = new URL(request.url).searchParams.get('shop');
  const stub = env.SHOPIFY_DURABLE_OBJECT.getByName(shop);
  const client = await stub.client('admin');
  return client.fetch(
    /* GraphQL */ `query Shop { shop { name } }`,
    {
      signal: request.signal,
      variables: undefined,
    },
  );
}

Follow Shopify App Proxy docs but import from ~/components/proxy instead of @shopify/shopify-app-remix/react

Branching convention

  • issue/# references an current issue / pull-request
  • extension/# is a non core feature extension

Copyright

Copyright (c) 2026 chr33s. See license for further details.

About

Shopify React Router (v7) app running on Cloudflare

Resources

Security policy

Stars

Watchers

Forks