Tired of maintaining expensive Kubernetes clusters, fine-tuning Auto-scaling groups on AWS, or wiring together complex CI/CD pipelines just to keep an e-commerce store alive? Welcome to the Zero DevOps era.
In this post, we dissect Aura Store — a production-grade Cloudflare Workers E-commerce platform built entirely on Edge infrastructure, powered by a Turborepo Monorepo. Everything you see below is drawn directly from the running codebase.
1. Turborepo Monorepo Architecture in Practice
Merging an Admin API and a Public API into a single backend is an invitation for privilege escalation bugs. Aura Store keeps them strictly apart:
apps/storefront-ui: Customer-facing storefront (Next.js 15+) — deployed to Cloudflare Pages for Edge rendering.apps/admin-ui: The back-office control panel (Vite/React) — deployed to Pages, strictly protected behind corporate SSO.apps/public-api: Cloudflare Worker that serves product listings, user carts, and handles checkout.apps/admin-api: A separate Cloudflare Worker with strict RBAC middleware. Unreachable from the public internet path.packages/contract: Zod schemas and OpenAPI specs — the single source of truth for all API payloads.packages/database: Drizzle ORM schema and migrations for Cloudflare D1.
Because both apps and packages share the same Turborepo workspace, a schema change in packages/contract or packages/database propagates to all consumers at build time — no manual version bumping required.
2. Multi-Environment Wrangler Configuration
Handling relational data, key-value storage, and queue processing at the edge requires careful binding definitions in the Worker configuration. The wrangler.toml file must define boundaries for development, staging, and production environments.
Here is the complete production-grade wrangler.toml config for public-api:
name = "public-api-worker"
main = "src/index.ts"
compatibility_date = "2026-06-01"
compatibility_flags = ["nodejs_compat"]
# Global Environment Variables
[vars]
ENVIRONMENT = "production"
API_VERSION = "v1"
# Cloudflare D1 Database Bindings
[[d1_databases]]
binding = "DB"
database_name = "ecommerce-db-prod"
database_id = "f052e46d-34e8-4217-b715-e21544f808db"
migrations_dir = "../../packages/database/migrations"
# Cloudflare KV Namespaces (Product Cache & Session Stores)
[[kv_namespaces]]
binding = "CACHE_KV"
id = "77c8e9b4122d4f3b8956e18dbcfb219e"
# Cloudflare R2 Buckets (Product Images & Attachments)
[[r2_buckets]]
binding = "PRODUCTS_R2"
bucket_name = "aura-storefront-assets-prod"
# Cloudflare Queue Bindings (Async Event Broker)
[[queues.producers]]
queue = "ecommerce-events-prod"
binding = "EVENT_QUEUE"
# -------------------------------------------------------------
# Staging Environment Override
# -------------------------------------------------------------
[env.staging]
name = "public-api-worker-staging"
[env.staging.vars]
ENVIRONMENT = "staging"
[[env.staging.d1_databases]]
binding = "DB"
database_name = "ecommerce-db-staging"
database_id = "d182e46d-55c8-4721-a115-e21544f999cc"
migrations_dir = "../../packages/database/migrations"
[[env.staging.kv_namespaces]]
binding = "CACHE_KV"
id = "22c8e9b4122d4f3b8956e18dbcfb319f"
[[env.staging.r2_buckets]]
binding = "PRODUCTS_R2"
bucket_name = "aura-storefront-assets-staging"
[[env.staging.queues.producers]]
queue = "ecommerce-events-staging"
binding = "EVENT_QUEUE"
Using this configuration, running a deployment is completely hands-off:
- For local development, Wrangler uses local SQLite:
wrangler dev - For staging deploy:
wrangler deploy --env staging - For production deploy:
wrangler deploy
3. Circumventing D1 Concurrency & SQLite Lock Timeouts
Cloudflare D1 is built on SQLite, which means it inherits SQLite’s core database locking behavior: a single writer model.
When thousands of users attempt to write to the database simultaneously during a product drop or flash sale, concurrent transactional writes will trigger SQLITE_BUSY: database is locked errors. This is because SQLite locks the entire database file during write transactions, causing queue congestion.
The Solution: Transaction Batching via the D1 Batch API
To prevent lock timeouts, we must minimize database write roundtrips. Instead of opening multiple transactions sequentially, we combine queries into a single batch using the native db.batch() API. This locks the database exactly once, executes all operations in memory, and writes them in a single file-system operation.
Below is the TypeScript implementation showing how to execute an edge checkout transaction batch with Drizzle ORM and D1.
import { drizzle } from "drizzle-orm/d1";
import { eq, sql } from "drizzle-orm";
import { orders, orderItems, inventoryStocks } from "../../packages/database/schema";
export interface Env {
DB: D1Database;
EVENT_QUEUE: Queue;
}
interface CheckoutPayload {
userId: string;
items: { sku: string; quantity: number; price: number }[];
}
export async function processEdgeCheckout(
env: Env,
payload: CheckoutPayload
): Promise<{ success: boolean; orderId?: string; error?: string }> {
const db = drizzle(env.DB);
const orderId = crypto.randomUUID();
const totalAmount = payload.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
try {
// 1. Prepare batch statements
// SQLite locking is mitigated by grouping verification, update, and logging
const statements: any[] = [];
// Statement A: Create the core order record
statements.push(
db.insert(orders).values({
id: orderId,
userId: payload.userId,
totalAmount: totalAmount,
status: "PROCESSING",
createdAt: new Date(),
})
);
// Statements B: Insert order items and deduct stock
for (const item of payload.items) {
// Record order line item
statements.push(
db.insert(orderItems).values({
orderId: orderId,
sku: item.sku,
quantity: item.quantity,
unitPrice: item.price,
})
);
// Deduct stock immediately. The WHERE clause prevents stock going negative
statements.push(
db
.update(inventoryStocks)
.set({
availableQty: sql`${inventoryStocks.availableQty} - ${item.quantity}`,
updatedAt: new Date(),
})
.where(
sql`${inventoryStocks.sku} = ${item.sku} AND ${inventoryStocks.availableQty} >= ${item.quantity}`
)
);
}
// 2. Execute statements in a single batch transaction block
// D1 guarantees all statements run atomically inside a single BEGIN IMMEDIATE/COMMIT block
const results = await env.DB.batch(statements);
// 3. Post-batch validation: Check if stock update succeeded
// Since SQL UPDATE returns success even if 0 rows matched the WHERE clause,
// we must verify that rows were actually updated.
// The index offset matches our batch array order.
let itemOffset = 2; // Order insert (0) + First Item insert (1) -> Stock update starts at index 2
for (let i = 0; i < payload.items.length; i++) {
const updateResult = results[itemOffset];
if (updateResult.meta.rows_written === 0) {
// If a row wasn't written, it means availableQty >= item.quantity failed
throw new Error(`Stock allocation failed for SKU: ${payload.items[i].sku}. Insufficient stock.`);
}
itemOffset += 3; // Step past: Item Insert (1) + Next Stock Update (1) + Next Item Insert (1)...
}
// 4. Publish background order completion event
await env.EVENT_QUEUE.send({
type: "ORDER_CREATED",
orderId: orderId,
timestamp: Date.now(),
});
return { success: true, orderId: orderId };
} catch (error: any) {
console.error("Checkout batch failed:", error.message);
return { success: false, error: error.message || "Unknown error during checkout." };
}
}
4. Automated Mobile SDK Generation
This is the sharpest edge of Aura Store’s architecture, and one that almost no Cloudflare tutorial covers. API contracts are defined once as Zod schemas in packages/contract. When a PR merges, the pipeline runs:
# Source: .github/workflows/openapi-sdk.yml
- name: Generate OpenAPI JSON Spec
run: pnpm --filter @ecommerce/contract build:openapi
- name: Generate Dart SDK (Flutter)
run: |
openapi-generator-cli generate \
-i ./packages/contract/openapi.json \
-g dart \
-o ./sdks/dart \
--additional-properties=pubName=aura_api_sdk,pubVersion=1.0.0
- name: Generate Swift SDK (iOS)
run: |
openapi-generator-cli generate \
-i ./packages/contract/openapi.json \
-g swift5 \
-o ./sdks/swift \
--additional-properties=projectName=AuraApiSDK
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: 'chore: auto-generate API SDKs from latest contracts'
branch: 'auto/sdk-update'
The pipeline opens a PR with the regenerated SDKs. Mobile teams merge it and ship — no reading changelogs, no manually diffing JSON payloads, no type mismatch bugs at runtime.
5. Zero DevOps Deployment: No CI/CD Pipelines to Maintain
Each app in the Turborepo maps to a separate Cloudflare project:
| App | Cloudflare Product | Build Command |
|---|---|---|
storefront-ui (Next.js) | Pages | pnpm run build --filter @ecommerce/storefront-ui |
admin-ui (Vite) | Pages | pnpm run build --filter @ecommerce/admin-ui |
public-api | Workers | Reads apps/public-api/wrangler.toml automatically |
admin-api | Workers | Reads apps/admin-api/wrangler.toml automatically |
Secrets are managed without .env files in CI. Set them once via the Wrangler CLI:
wrangler secret put STRIPE_WEBHOOK_SECRET
wrangler secret put JWT_SECRET
For local Stripe Webhook testing, forward events to the running Worker dev server:
stripe listen --forward-to localhost:8787/api/webhooks/stripe
The Stripe CLI prints a STRIPE_WEBHOOK_SECRET value; paste it into apps/public-api/.dev.vars. The dev environment mirrors production exactly — same code path, same Worker runtime, same D1 schema.
6. FAQ: Cloudflare E-commerce Architecture
What is a Zero DevOps architecture?
Zero DevOps is a model where engineers spend 100% of their time writing product code rather than operating servers. With the Cloudflare ecosystem, there is no OS to patch, no scaling group to tune, and no load balancer certificate to rotate. The platform handles availability, global distribution, and DDoS mitigation automatically.
Why use Cloudflare Workers instead of AWS Lambda?
The fundamental difference is the runtime model. AWS Lambda runs inside a lightweight container that must cold-start — a virtualisation layer that adds 100ms–500ms on the first request. Cloudflare Workers run on V8 isolates: the same engine that powers Chrome, with near-zero cold start overhead. On a globally distributed network of 300+ cities, most users receive a sub-millisecond response from an edge node physically close to them — without any geo-routing configuration.
| AWS Lambda | Cloudflare Workers | |
|---|---|---|
| Runtime | Container (cold start possible) | V8 isolates (cold start eliminated) |
| Global latency | Origin-dependent | Sub-millisecond at 300+ PoPs |
| Infrastructure config | VPC, subnets, IAM | None — deploy via wrangler |
| Pricing model | Per-invocation + duration | Per-request (generous free tier) |
How do you handle relational data at the Edge?
Instead of routing every query back to a centralised RDS instance in us-east-1, use Cloudflare D1. D1 is a globally distributed SQL database built on SQLite. Drizzle ORM wraps D1 with a fully type-safe query builder, so your schema and queries are validated at compile time — not at runtime in production.
