SkillAgentSearch skills...

Shopflare

Shopify React Router (v7) app running on Cloudflare

Install / Use

/learn @chr33s/Shopflare
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

[!NOTE]

  • v2 branch tracks Polaris React changes
  • shopify-app-js branch tracks Shopify 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,
    },
  );
}

proxy.tsx

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.

View on GitHub
GitHub Stars62
CategoryDevelopment
Updated7h ago
Forks7

Languages

TypeScript

Security Score

80/100

Audited on Apr 3, 2026

No findings