I built an entire retail platform on Cloudflare's edge — here's the map


Most AI demos are a chat box bolted onto a landing page. I wanted the opposite: a complete retail platform — catalog, carts, checkout, realtime inventory, customer accounts, an AI stylist that can actually look things up — running entirely on Cloudflare, with every claim verified in production. This post is the map. The next thirteen go one layer deep at a time.

The platform is live at retail.mattwynne.solutions. It is a demo, so the data is synthetic and the scale assumptions are demo-grade — but every feature on it is real, deployed, and was exercised end-to-end while building it.

The map: two Workers, twelve products

Product What it does here
Workers Two of them: cloudflare-retail-demo (storefront + API + agent) and lumina-internal-mcp (the agent’s tool server)
Static Assets The storefront SPA is served straight from the retail Worker — zero-config routing, no origin server
D1 One database for everything durable: products, carts, orders, users, sessions
Workers AI The stylist’s models, the bge embedding model for search, and flux-1-schnell for generating product photos
AI Gateway Model routing by customer tier, plus a firewall (Guardrails + DLP) in front of every prompt
Vectorize Semantic product search — 768-dim vectors, with D1 always kept as the source of truth
Durable Objects One RetailHub object: live “X people viewing” counts over WebSockets, and atomic checkout reservations
AI Search (AutoRAG) A managed index over 11 internal policy docs, so the bot answers with citations instead of inventing policy
R2 Product photos (uploaded from the admin panel, generated by Workers AI) and the KB source archive
Turnstile Bot verification on signup, sign-in and checkout
Cloudflare Access SSO on /admin, and the service-token identity the agent uses to reach its tools
MCP Server Portal Governed tool access: the agent calls its tools through the portal, exactly like a third-party agent would

How it works

The whole thing is wired with bindings — the config file is a surprising amount of the story:

// wrangler.jsonc (trimmed)
{
  "main": "src/index.ts",
  "assets": { "directory": "./public", "binding": "ASSETS" },
  "ai":    { "binding": "AI" },
  "d1_databases":  [{ "binding": "DB", "database_name": "lumina-users" }],
  "vectorize":     [{ "binding": "VECTORIZE", "index_name": "lumina-product-search" }],
  "r2_buckets":    [{ "binding": "MEDIA", "bucket_name": "lumina-media" }],
  "durable_objects": { "bindings": [{ "name": "RETAIL_HUB", "class_name": "RetailHub" }] },
  "observability": { "enabled": true, "traces": { "enabled": true } },
  "routes": [{ "pattern": "retail.mattwynne.solutions", "custom_domain": true }]
}

The routing table in the Worker is a plain if-chain over url.pathname — API routes first, server-rendered pages next, static assets as the fallback:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname === '/api/chat') return handleChat(request, env);
    if (url.pathname === '/api/products') return handleListProducts(request, env);
    if (url.pathname === '/api/checkout') return handleCheckout(request, env);
    // ... agent mirrors, server-rendered pages, admin ...
    return env.ASSETS.fetch(request);  // the SPA
  },
};

Three design decisions carry the rest of the architecture:

1. D1 is the single source of truth. Vectorize holds derived vectors, AI Search holds an index of policy docs, the Durable Object holds a stock cache — and every one of them is re-hydrated from D1 before anything is shown or acted on. Prices and stock are never stale because they only live in one place.

2. The agent is a remote MCP client, not a service binding. It could call its tool server privately, but it deliberately goes over the public MCP protocol through Cloudflare’s MCP Server Portal — the same path Claude, ChatGPT or Cursor would take. That makes the governance story (portal toggles, Access identity, request logs) a platform demo rather than a private shortcut.

3. Graceful degradation everywhere. AI failure → a local deterministic mock. Vectorize unavailable → SQL keyword fallback. Portal unreachable → the bot says the capability is unavailable instead of inventing an answer. A demo that breaks the moment one product hiccups is not a demo you can trust.

What the demo shows

One edge deployment serves several audiences at once: shoppers get the SPA, admins get an Access-protected panel at /admin, and machines get an agent-ready surface — /robots.txt, /sitemap.xml, /llms.txt, /openapi.json, JSON mirrors under /api/ai/*, and server-rendered product pages with JSON-LD. Later posts cover each.

Evidence: what to capture

  • Cloudflare dashboard → Workers & Pages: the two production Workers (cloudflare-retail-demo, lumina-internal-mcp) → save as 01-workers-list.png
  • Cloudflare dashboard → Storage & Databases → D1 → lumina-users: tables + row counts → 01-d1-dashboard.png
  • Live storefront https://retail.mattwynne.solutions — home page with product cards → 01-storefront-home.png
  • https://retail.mattwynne.solutions/api/health → {"status":"ok",...} → 01-health-json.png
  • Export the architecture diagram: open Blog/src/assets/diagrams/edge-retail-platform.excalidraw in the Excalidraw editor, tweak if you like, export PNG → 01-edge-retail-platform/edge-retail-platform.png (the image above ships from that file)

Key takeaways

  • Bindings are the integration story: six JSON entries wire the Worker to every product on the platform.
  • One source of truth (D1) plus derived indexes keeps the demo honest — and keeps stale data out of the UI.
  • Choosing the public path (portal + MCP protocol) for the agent is what turns “an app that calls its own backend” into a platform-governance demo.