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 as01-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.excalidrawin 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.