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:

AppCloudflare ProductBuild Command
storefront-ui (Next.js)Pagespnpm run build --filter @ecommerce/storefront-ui
admin-ui (Vite)Pagespnpm run build --filter @ecommerce/admin-ui
public-apiWorkersReads apps/public-api/wrangler.toml automatically
admin-apiWorkersReads 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 LambdaCloudflare Workers
RuntimeContainer (cold start possible)V8 isolates (cold start eliminated)
Global latencyOrigin-dependentSub-millisecond at 300+ PoPs
Infrastructure configVPC, subnets, IAMNone — deploy via wrangler
Pricing modelPer-invocation + durationPer-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.