After this lesson you'll be able to look at a real product spec, assign each concern (auth, files, search, async work, live collaboration, onboarding) to the correct Cloudflare product, and explain why the alternatives you didn't pick would have been the wrong fit.
This lesson introduces no new mechanics. Every product below was covered in depth in an earlier lesson — this is where they stop being 25 separate facts and become one system. We'll design a single app, Driftnotes, and walk through it lesson-by-lesson, naming which product owns which concern and linking back to where you learned the mechanics.
Every box below is a product you've already met. The arrows are real request/data flows, not aspirational ones — this is a shape you could actually deploy today.
┌─────────────────────────┐
│ Pages (frontend shell) │ L11, L12
│ React/Next UI + routing │
└────────────┬─────────────┘
│ fetch()
▼
┌────────────────────────────────────────┐
│ Worker API (edge) │ L2, L3, L4
│ routing, auth middleware, orchestration │
└───┬────────┬────────┬────────┬─────┬────┘
│ │ │ │ │
session lookup │ │ │ │ │ logs/traces
▼ │ │ │ ▼
┌───────────┐ │ │ │ ┌──────────────┐
│ Workers KV│ │ │ │ │Observability │ L7
│ sessions, │ │ │ │ │ tail, metrics│
│ config │ │ │ │ └──────────────┘
└───────────┘ │ │ │
L25 │ │ │
▼ │ │
┌───────────┐ │ │
│ D1 │ │ │
│ users, │ │ │
│ boards, │ │ │
│ notes │ │ │
└───────────┘ │ │
L20, L21 │ │
▼ │
┌───────────┐ │
│ R2 │ │
│ file/image│ │
│ uploads │ │
└───────────┘ │
L23, L24 │
▼
┌──────────────────────┐ ┌───────────────┐
│ signup → Queues │ │ Durable Object │ L5, L6
│ → Workflows onboard │ │ per live board │
│ (welcome email, DLQ)│ │ (WebSocket hub)│
└──────────────────────┘ └───────────────┘
L26 L9, L10
┌───────────────────────────────────────────┐
│ Semantic search path │
│ note text → Workers AI (embed) L18 │
│ → Vectorize (store/query) L17 │
│ search query → AI Gateway → LLM answer L16 │
└───────────────────────────────────────────┘
Nothing here is exotic. It's the same pattern from the platform map: a Worker at the center, talking to the storage product that fits the shape of each piece of data, with async and stateful concerns peeled off into the products built for them.
Driftnotes' UI is a Pages project (Pages: static and full-stack) — it gets git-based deploys and preview URLs for free, and its functions/ routes handle simple reads. The real API — auth, orchestration across D1/R2/Queues — lives in a standalone Worker, because Pages vs. Workers: when to use which is exactly the call this app has to make: UI framework hosting goes to Pages, non-trivial backend logic goes to a Worker. Inside that Worker, routing and auth middleware follow the pattern from Workers routing and middleware — a small router dispatching to handlers, with a middleware step that resolves the session before anything else runs. The Worker itself was scaffolded and deployed the way Deploy your first Worker describes, and every binding below (KV, D1, R2, Queues, Vectorize, AI) is wired up exactly as Workers bindings and storage taught: declared in wrangler.jsonc, accessed off env at request time.
When a request arrives with a session cookie, the auth middleware does one KV read (Workers KV) to resolve session:<token> to a user ID — fast, globally-replicated reads are exactly what a session lookup on every request needs, and KV's eventual consistency is a non-issue here because sessions are written once at login and read many times after. Feature flags (e.g. "is semantic search enabled for this account") live in KV too, for the same reason: read-heavy, rarely-written, fine to be a few seconds stale.
Users, boards, notes, and board memberships are relational by nature — a note belongs to a board, a board has many members, a user has many boards — which is precisely the shape D1: relational database and D1 migrations and relationships exist for. Foreign keys and joins (fetch a board with its notes and member list in one query) are natural in D1 and would be awkward to fake in KV or R2. Driftnotes has no existing external database to reach from a Worker, so Hyperdrive doesn't apply here — that's the tool for the "I already run Postgres somewhere else" case, not this one.
Attachments (screenshots, PDFs pinned to a note) are blobs, not rows, so they go to R2 (R2: object storage), keyed by a UUID with the D1 row storing that key as a foreign reference. Uploads use a presigned URL so the browser uploads directly to R2 without proxying file bytes through the Worker, and an R2 event notification triggers a thumbnail job — the exact pattern from R2 presigned URLs and public buckets.
Signup shouldn't block on sending a welcome email or provisioning a default board. The signup handler writes one message to a queue and returns immediately, exactly as in Queues: async processing — a consumer Worker picks the message up, with a dead letter queue configured so a flaky email provider can't silently swallow signups.
The consumer's job isn't a single action, though — new-user onboarding is: create a default board, send a welcome email, wait a day, send a "here's how to use search" tip email, wait three more days, send a re-engagement nudge if the user hasn't created a note yet. That's a durable, multi-day, resumable sequence, which is what Workflows: durable execution and Workflows error handling are for — each step is checkpointed, so a Cloudflare-side restart or a transient email-provider failure doesn't restart onboarding from step one, and a failed step retries with backoff instead of aborting the whole sequence.
When two teammates open the same board, their edits need one consistent order and a place to hold "who's currently connected." That's a single-instance actor problem, so each board gets one Durable Object instance keyed by board ID, holding the WebSocket connections and broadcasting edits — the same design as Durable Objects fundamentals and Durable Objects and WebSockets. This is deliberately not built on D1 or KV: neither gives you the single-writer, in-memory coordination a live multi-cursor board needs.
"Find that thing I saved about caching" is a meaning match, not a keyword match, so it needs embeddings. When a note is saved, the Worker calls Workers AI (Workers AI inference) to embed its text, then stores that vector in Vectorize alongside the note ID (Vectorize: embeddings). A search query gets embedded the same way and matched against stored vectors to retrieve candidate notes. If Driftnotes also has an AI agent feature — "summarize everything I saved this week" — that agent loop follows Workers AI agents. Every one of those LLM calls, whether to Workers AI or an external provider, is routed through AI Gateway rather than called directly — it gives caching (so re-running the same summary doesn't re-bill the model), rate limiting, and per-feature cost visibility without touching the calling code.
None of the above is worth much once it's in production if you can't see what it's doing. Every Worker in this stack — API, Queues consumer, Workflow, Durable Object — emits logs and is tailable the way Observability for Workers describes, so a failed embedding call or a stuck onboarding step shows up in real-time tail rather than as a silent gap in the data.
Not every product from the course belongs in every app, and forcing all 16 in would be a worse design, not a more complete one. Driftnotes has no video, so Stream and Realtime don't appear; its images are simple attachments, not a resize-heavy delivery pipeline, so Images is a reasonable later addition rather than a launch requirement; and it has no need to run other people's code, so Workers for Platforms stays out of scope. Picking the right five or six products for a given app is the actual skill — this capstone's point is that you now have all 16 to choose from, with a working mental model for when each one is the right call.
Cloudflare Developer Platform docs — the umbrella docs site linking every product covered across this course; each individual lesson above links its own specific primary source for pricing and mechanics detail.
Queues handles getting the very first side effect (kick off onboarding) off the signup request's critical path — the signup Worker enqueues a message and returns immediately. But onboarding itself isn't one action, it's a multi-step, multi-day sequence (create board, welcome email, wait a day, tip email, wait more, maybe a nudge) that needs to survive restarts and retry individual steps without starting over — that durability and checkpointing is what Workflows adds on top. Queues gets the work off the request path; Workflows keeps the resulting multi-step process reliable over time.