Capstone · Synthesis

Capstone: ship a full-stack app across the whole platform

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.

Driftnotes, in one sentence. A link-sharing and notes app: users sign up, save notes and links to shared "boards," upload attachments, get a welcome email, search their notes semantically ("find that thing about caching I saved last month"), and can co-edit a board live with teammates.

The architecture, end to end

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.

Concern by concern

App shell and routing — Pages + Workers

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.

Sessions and config — Workers KV

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.

Relational data — D1

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.

File uploads — R2

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.

Async work off the request path — Queues

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.

Multi-step onboarding — Workflows

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.

Live collaborative boards — Durable Objects

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.

Semantic search — Vectorize + Workers AI, fronted by AI Gateway

"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.

Logs and debugging — Observability

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.

The one-line assignment, for reference:
Pitfall: reaching for the product you learned most recently instead of the one that fits. The most common mistake once you know all 16 products isn't ignorance, it's misapplication — e.g. putting session data in D1 (works, but slower and pure overhead versus KV), or trying to coordinate live collaboration through Queues (messages are processed asynchronously in batches, with no shared in-memory state between consumers — the opposite of what a live cursor needs). When two products could technically do the job, the tie-breaker is the access pattern: read-heavy and eventually-fine-if-stale → KV; relational with joins → D1; needs one consistent in-order actor → Durable Objects; can happen "a bit later, off the request path" → Queues; is a checkpointed multi-step sequence → Workflows. If you can't articulate which access pattern you have, that's the sign to re-read the relevant lesson rather than guess.

What this app deliberately leaves out

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.

Primary source

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.

Two teammates are editing the same Driftnotes board at once and need their cursor positions and edits to stay in one consistent order in real time. Which product is the right fit, and why not the runner-up?
Without scrolling up: in Driftnotes, why does the welcome-email flow use both Queues and Workflows instead of just one or the other?
Reveal

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.

Anything above unclear — why a particular product was chosen over an alternative, or how two of these pieces would actually wire together in code — ask your AI teacher. This is the lesson where it's worth asking "what if I used X instead of Y here" and seeing where that reasoning breaks down.
← Previous: Decouple work with Queues Back to the map →