Building a Shopify App on Cloudflare Workers
An edge-first architecture for Shopify apps: OAuth on Workers, HMAC-verified webhooks, session storage without a Postgres bill, and the constraints you need to design around.
Quick answer
You can run a production Shopify app entirely on Cloudflare Workers: handle OAuth and App Bridge session tokens in a Worker, verify every webhook with an HMAC timing-safe comparison, store shop sessions in KV or D1, and move heavy media work to Queues. The main constraints are CPU time per request and the absence of Node-native binaries, which is why long tasks must be queued rather than inlined.
Why the edge suits Shopify apps
A Shopify app is mostly small, bursty, globally distributed traffic: embedded admin loads, webhook deliveries and storefront asset requests. That is a poor fit for an always-on container in one region and a very good fit for a Worker that runs next to the merchant.
OAuth without a server
- Merchant hits /auth?shop=store.myshopify.com — the Worker validates the shop domain against a strict pattern.
- Redirect to Shopify with a signed state stored in KV with a short TTL.
- Shopify calls back; the Worker verifies HMAC and state, then exchanges the code for an offline token.
- Persist the token per shop, keyed by shop domain, and redirect into the embedded admin.
Webhooks: verify first, parse second
const raw = await request.text();
const digest = await crypto.subtle.sign(
"HMAC",
await hmacKey(env.SHOPIFY_API_SECRET),
new TextEncoder().encode(raw),
);
const expected = btoa(String.fromCharCode(...new Uint8Array(digest)));
const given = request.headers.get("x-shopify-hmac-sha256") ?? "";
if (!timingSafeEqual(expected, given)) {
return new Response("Invalid signature", { status: 401 });
}
const payload = JSON.parse(raw);Respond 200 fast and do the work in a queue. Shopify retries aggressively on slow responses, and a webhook handler that also resizes images will eventually create duplicate work.
Where state lives
| Data | Store | Reason |
|---|---|---|
| Shop sessions and tokens | KV or D1 | Read-heavy, tiny, per-shop |
| Job state and usage counters | D1 or Durable Objects | Needs transactions or coordination |
| Optimised images | R2 | No egress fees, cache-friendly |
| Rate limits per shop | Durable Object | Single point of serialisation |
Constraints to design around
- No native Node binaries — sharp, canvas and child_process are unavailable; use WASM or a queue consumer.
- CPU time per request is limited; long loops belong in Queues, not handlers.
- Read environment inside the handler, never at module scope.
- Everything must be bundled at build time — no runtime module resolution.
Billing and mandatory webhooks
Implement the GDPR webhooks on day one — customers/redact, shop/redact and customers/data_request — because app review will reject you without them. Reconcile subscription state from Shopify's billing API on every admin load rather than trusting a locally cached flag.
Checklist
- Shop domain validated with a strict regex on every entry point
- OAuth state stored server-side with a TTL
- HMAC verified on the raw body before parsing
- Webhook handlers return 200 in under a second and enqueue work
- Sessions in KV/D1, media in R2
- Mandatory GDPR webhooks implemented
- Billing state reconciled from Shopify, not cached locally
- No Node-only dependencies in the Worker bundle
Common mistakes
- Parsing the webhook body before verifying its signature.
- Doing image or export work inside the webhook handler.
- Storing access tokens in a client-readable cookie or in the embedded frame.
- Reading process.env at module scope, which is undefined on Workers.
- Assuming an npm package works at the edge because it installs cleanly.
Frequently asked questions
Can a Shopify app run fully on Cloudflare Workers?
Yes. OAuth, embedded admin rendering, GraphQL Admin API calls, webhooks and billing all work on Workers. Heavy or long-running work moves to Queues with an R2 bucket for output.
KV or D1 for shop sessions?
KV for pure key-value reads at very high volume with eventual consistency; D1 when you need relational queries, joins or transactional updates such as usage metering.
How do I process images without sharp?
Use Cloudflare Images or a WASM codec inside a queue consumer. Native modules cannot be bundled into a Worker.
Summary
Verify everything, keep handlers short, store sessions in KV or D1, push media and long work into Queues plus R2, and design around Worker CPU and bundling constraints from the first commit.
working on something like this?