Building a RAG Chatbot on Cloudflare Workers (Vectorize + D1 + Workflows) A developer built chatbot-with-rag, a minimal retrieval-augmented generation chat demo that runs entirely on Cloudflare Workers using Vectorize, D1, and Workflows. The project links D1's auto-increment chunk id to the corresponding Vectorize vector id for retrieval, and uses Cloudflare Workflows' step.do() durable checkpoints to give each chunk its own retry rather than re-running an entire batch on failure. I first saw RAG in 2024, at a hackathon. The moment that stuck with me was realizing an LLM could "search" through documents I'd just uploaded and answer questions about them - not from its training data, from my content. I was genuinely astonished; it felt like a different category of capability than just chatting with a model. Today, that's not a novelty anymore - it's close to a baseline expectation for any chatbot that needs to answer questions about content it wasn't trained on. A chatbot only knows what it was trained on - ask it about your own docs, your own product, your own notes, and it either hallucinates an answer or tells you it doesn't know. Retrieval-Augmented Generation RAG is the standard fix: before the model answers, you go find the actual relevant text and hand it over as context. I wanted to build the smallest version of that pattern that still behaves honestly - retrieves the right thing, admits when it can't, and doesn't lose data halfway through ingesting a document. This is a walkthrough of chatbot-with-rag https://github.com/palermo-777/chatbot-with-rag , a minimal RAG chat demo running entirely on Cloudflare: No LangChain agent framework, no vector DB to self-host, no separate backend - one Worker, four bindings. Two flows, running through the same Worker. The two stores are linked by a shared id: D1's auto-increment id for a chunk is the exact same value used as that chunk's Vectorize vector id. A similarity search gives you back an id; a D1 lookup on that id gives you the actual text. No metadata duplication, no second index to keep in sync - just one id, two stores, one source of truth each for what they're good at Vectorize for "what's similar," D1 for "what does it actually say" . The obvious version of ingestion is: split the doc, loop over the chunks, embed and insert each one, done. The problem is what happens when chunk 7 of 12 fails - a rate-limited embedding call, a transient D1 failure, doesn't matter. A plain loop either crashes the whole request losing all 12 chunks or needs you to implement retry logic. Cloudflare Workflows solve this by making each step.do call a durable checkpoint. If a step fails, only that step retries - everything before it is already saved and never redone: export class RAGWorkflow extends WorkflowEntrypoint