Note
v2 branch tracks Polaris React changes
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.
- 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:
- Embedded app use-case
- New Embedded Auth Strategy
- Managed Pricing
- Optimized for Cloudflare stack
- Tested - (unit, browser, e2e)
Familiarity with React, ReactRouter, Cloudflare, Shopify conventions.
- Cloudflare account
- Shopify Partner account
- Node.js & NPM see package.json#engines
brew install node@22 - cloudflared cli
brew install cloudflared(optional) - 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" ]
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# 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}npm run build
npm run deployTo split environments see Cloudflare and Shopify docs.
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,
});
}/// 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
issue/#references an current issue / pull-requestextension/#is a non core feature extension
Copyright (c) 2026 chr33s. See license for further details.