Scaffold Shopify apps that run on Cloudflare Workers.
npx scf init my-appThe default Shopify CLI generates apps for Node.js. This CLI generates apps optimized for Cloudflare's edge network:
| Node.js (default) | Cloudflare Workers | |
|---|---|---|
| Cold starts | 200-500ms | ~0ms |
| Global latency | Single region | Edge everywhere |
| Database | Prisma + SQLite | D1 (SQLite at edge) |
| Sessions | Prisma adapter | KV (global) |
| Scaling | Manual | Automatic |
| Cost | $5+/mo minimum | Free tier generous |
# Create a new app
npx scf init my-shopify-app
cd my-shopify-app
# Install dependencies
npm install
# Configure credentials
cp .dev.vars.example .dev.vars
# Edit .dev.vars with your Shopify API credentials
# Set up Cloudflare resources (D1 database, KV namespace)
npm run setup
# Start development
npm run devmy-shopify-app/
├── app/
│ ├── db/
│ │ ├── client.ts # Drizzle + D1
│ │ └── schema.ts # Your database schema
│ ├── routes/
│ │ ├── _index.tsx # Main app page
│ │ ├── auth.$.tsx # OAuth callback
│ │ ├── auth.login/ # Login page
│ │ └── webhooks.*.tsx # Webhook handlers
│ └── shopify.server.ts # Shopify configuration
├── workers/
│ └── app.ts # Cloudflare Worker entry
├── wrangler.json # Cloudflare configuration
└── shopify.app.toml # Shopify app configuration
- Runtime: Cloudflare Workers
- Framework: React Router v7
- Database: Cloudflare D1 + Drizzle ORM
- Sessions: Cloudflare KV
- UI: Shopify Polaris
npm run dev # Start with Shopify CLI (recommended)
npm run dev:wrangler # Start with Wrangler onlynpm run db:generate # Generate migration from schema changes
npm run db:migrate # Apply migrations locally
npm run db:studio # Open Drizzle Studionpm run deploy # Deploy everything (migrations + Worker + Shopify)Or step by step:
npm run db:migrate:prod # Apply migrations to production D1
npm run deploy:cf # Deploy to Cloudflare Workers
npm run deploy:shopify # Deploy Shopify app configLocal development (.dev.vars):
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SHOPIFY_APP_URL=https://your-tunnel.trycloudflare.com
SCOPES=write_products,read_productsProduction (Cloudflare secrets):
wrangler secret put SHOPIFY_API_KEY
wrangler secret put SHOPIFY_API_SECRET
wrangler secret put SHOPIFY_APP_URL
wrangler secret put SCOPESAfter running npm run setup, update wrangler.json with your resource IDs:
{
"d1_databases": [{
"binding": "DB",
"database_id": "your-d1-id"
}],
"kv_namespaces": [{
"binding": "SESSION_STORAGE",
"id": "your-kv-id"
}]
}Edit app/db/schema.ts:
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const products = sqliteTable("products", {
id: integer("id").primaryKey({ autoIncrement: true }),
shopId: text("shop_id").notNull(),
title: text("title").notNull(),
syncedAt: integer("synced_at", { mode: "timestamp" }),
});Then generate and apply the migration:
npm run db:generate
npm run db:migrateEvery route that needs Shopify authentication follows this pattern:
import type { LoaderFunctionArgs } from "react-router";
import { initializeShopify, authenticate } from "../shopify.server";
import { getDbFromContext, type CloudflareEnv } from "../db/client";
export const loader = async ({ request, context }: LoaderFunctionArgs) => {
const env = (context as { cloudflare: { env: CloudflareEnv } }).cloudflare.env;
// Initialize Shopify (required before authenticate)
initializeShopify(env);
// Authenticate
const { session, admin } = await authenticate.admin(request);
// Get database
const db = getDbFromContext(context as { cloudflare: { env: CloudflareEnv } });
// Query with shop isolation
const data = await db.query.products.findMany({
where: (products, { eq }) => eq(products.shopId, session.shop),
});
return { data };
};MIT