🔗 Live preview: cloudflare-shopify-starter-template.ali-d43.workers.dev/preview (opens without a Shopify login).
Keywords: Shopify embedded app, Cloudflare Workers, Hono, Drizzle ORM, D1, KV, R2, React Polaris, session token auth, boilerplate, template, starter
A production-ready Shopify embedded app boilerplate / template built on Cloudflare Workers. Features session-token auth, Hono routing, Drizzle ORM on D1, KV-backed session storage, R2 file storage, and a React 18 + Shopify Polaris frontend — all running at the edge with zero cold starts.
Stack: Cloudflare Workers (Hono) · D1 + Drizzle · KV (sessions) · R2 (files) · React 18 + Vite · Shopify Polaris + App Bridge.
What's wired up out of the box:
- Shopify OAuth + session-token auth (all
/api/*routes are protected by middleware) - KV-backed Shopify session storage
- D1 + Drizzle with a single
shopify_shoptable to extend - Install / uninstall lifecycle, including
app/uninstalledwebhook - One example protected API route (
GET /api/example) and one Polaris page that fetches it
What's commented out as opt-in (in wrangler.jsonc): Cloudflare Queues, Durable Objects, Cron triggers.
Browser (Shopify Admin)
└── App Bridge (session token JWT)
└── Cloudflare Worker (Hono)
├── /shopify/install → OAuth install start
├── /shopify/callback → OAuth callback, session saved to KV
├── /api/* → requireShop middleware (JWT verification)
│ └── GET /api/example → queries D1, returns JSON
└── /* (static assets) → React + Polaris SPA (served via [assets])
- Install flow:
/shopify/install?shop=<shop>→ Shopify consent →/shopify/callback→ session persisted inSESSION_KV. - Session-token flow: App Bridge embeds a short-lived JWT in every API request header; the
requireShopmiddleware verifies it and attaches the shop to the request context. - Data layer: Drizzle ORM on D1 (SQLite). All tables cascade-delete on shop removal (
SHOP_REDACTGDPR pattern).
- Node 20+
- A Cloudflare account (free tier is fine)
- A Shopify Partner account + a development app
npm install# Database
wrangler d1 create shopify-on-cloudflare-db
# → copy the returned database_id into wrangler.jsonc
# KV namespace for Shopify sessions
wrangler kv:namespace create SESSION_KV
# → copy the returned id into wrangler.jsonc
# R2 bucket for file storage
wrangler r2 bucket create shopify-on-cloudflare-filesOpen wrangler.jsonc and replace the YOUR_* placeholders (database_id, KV id) with the values above. (No account_id needed — Wrangler uses your logged-in account.)
In the Shopify Partner dashboard, create an app and copy its client ID and secret.
npx wrangler secret put SHOPIFY_CLIENT_ID
npx wrangler secret put SHOPIFY_API_SECRET
npx wrangler secret put HOST # bare hostname, no protocol, e.g. shopify-on-cloudflare.<you>.workers.devCopy .env.example to .env and fill in VITE_SHOPIFY_CLIENT_ID (the public client ID).
For local development, also copy .dev.vars.example to .dev.vars and fill in the same Shopify credentials — wrangler dev (via the Vite plugin) loads them from there, so you don't need wrangler secret put locally.
After your first deploy (or when using a tunnel locally), set these values in the Shopify Partner Dashboard under App setup:
| Field | Value |
|---|---|
| App URL | https://<your-worker-host>/shopify/install |
| Allowed redirection URLs | https://<your-worker-host>/shopify/callback |
Replace <your-worker-host> with your *.workers.dev hostname (or custom domain).
npm run setup # local — apply D1 migrations for `npm run dev` (alias for d1:migrate:local)
npm run d1:migrate # remote — apply D1 migrations to your deployed D1First time on a fresh clone, do the one-time setup — copy local vars and apply local migrations:
cp .dev.vars.example .dev.vars # then fill in your Shopify credentials
npm run setup # apply D1 migrations to the local databaseThen a single command runs the whole app — React frontend and Worker backend — on one port, via the Cloudflare Vite plugin:
npm run devEverything is served at http://localhost:5173, with the Worker running in the real workerd runtime and D1, KV, and R2 bound locally.
Shopify OAuth requires a public HTTPS URL even in development. Use the included
cloudflaredtunnel to expose your local server:npm run dev:tunnel # exposes http://localhost:5173 via a cloudflared HTTPS tunnelCopy the printed
https://*.trycloudflare.comURL and usehttps://*.trycloudflare.com/shopify/installas your App URL andhttps://*.trycloudflare.com/shopify/callbackas your Allowed redirection URL in the Shopify Partner Dashboard for the duration of the dev session.
Seed a test shop and hit the example endpoint:
wrangler d1 execute shopify-on-cloudflare-db --local --command "
INSERT OR IGNORE INTO shopify_shop (id, myshopify_domain, domain, name, status, install_date)
VALUES ('test-shop-id', 'mystore.myshopify.com', 'mystore.myshopify.com', 'Test Store', 'installed', datetime('now'));
"
curl http://localhost:5173/api/example -H "x-shop-domain: mystore.myshopify.com"
# → {"shopId":"test-shop-id","now":"2026-..."}npm test # unit tests (Vitest)
npm run test:e2e # end-to-end smoke tests (Playwright)The Playwright suite boots the app with npm run dev and checks the health endpoint, that protected /api/* routes reject unauthenticated requests, and that the SPA shell is served.
npm run deploynpm run deploy builds the frontend and Worker together (vite build) and ships them with wrangler deploy.
- Add tables: edit
src/db/schema.ts, runnpm run d1:generate, thennpm run d1:migrate. New tables should have a non-nullshopIdFK toshopify_shopwithonDelete: 'cascade'(GDPRSHOP_REDACTpattern). - Add routes: drop a new file in
src/routes/, export a Hono router, wire it insrc/index.ts./api/*paths are auth-guarded automatically. - Add background jobs: uncomment the queues / durable-objects / cron blocks in
wrangler.jsoncand add the matchingqueue/scheduledexport tosrc/index.ts. - Add a webhook topic: extend
TOPICSinsrc/lifecycle/webhooks.tsand dispatch it in the handler.
The frontend bundles @bugsnag/js and @bugsnag/plugin-react. To enable it, set your API key:
# .env (Vite/frontend)
VITE_BUGSNAG_API_KEY=your-bugsnag-api-keyIf you do not use Bugsnag, you can safely remove @bugsnag/js and @bugsnag/plugin-react from package.json and delete the Bugsnag initialisation call in the frontend entry point.
Contributions, bug reports, and feature requests are welcome.
- Fork the repository and create a branch from
main. - Make your changes, keeping commits focused and atomic.
- Follow the commit message convention enforced by
commitlint(Conventional Commits:feat:,fix:,chore:, etc.). - Open a pull request — describe what you changed and why.
For significant changes, open an issue first to discuss the approach.
Need a custom Shopify app or storefront built on Cloudflare? See Devkind's Shopify development services.
Built by Devkind. Maintained by the Devkind Engineering team (hello@devkind.com.au).
MIT — see LICENSE.
