This project is an LLM-powered chatbot with a custom RAG system, deployed on Cloudflare's edge network. Check out the live demo at chienliu.com — the chatbot can answer questions about the author, Chien Liu 😺
- Cloudflare Workers: Edge runtime hosting the chatbot API
- Workers AI: LLM inference for chat responses
- D1: Relational store for RAG note metadata
- Vectorize: Vector store for RAG embeddings/retrieval
- Workflows: Orchestrates the RAG note sync process
- Hono: Lightweight router for the Worker
The chatbot supports multi-turn conversations. It can answer follow-up questions like "Elaborate on that" or "What else?" because each request optionally carries an array of prior messages.
| Frontend storage (this project) | Backend storage | |
|---|---|---|
| Where history lives | Browser memory — sent on each request | Server session or database |
| Worker statefulness | Stateless — each request is self-contained | Stateful — Worker must read/write per-user state |
| Implementation cost | None — client accumulates and sends history | Session IDs, a KV/D1 table, and an eviction strategy |
| Privacy | History never persists beyond the tab session | History persists server-side |
| Cloudflare cost | No extra bindings or reads/writes | D1 or KV reads and writes on every turn |
For a simple personal chatbot, frontend storage is the better call. Leveraging frontend storage is simpler, cheaper, and more privacy-compliant.
- This repository is integrated with Cloudflare Workers.
- The deployed Worker is built from the code in
src/, withsrc/worker.jsas the entrypoint. - The production route is
https://api.chienliu.com/chatbot. - The local maintenance assets in
notes/andscripts/support RAG content management, but they are not the Worker code that gets deployed fromsrc/.
POST /chatbotis rate limited per client IP using the native Cloudflare Workers Rate Limiting binding (CHATBOT_RATE_LIMITER, configured inwrangler.jsonc).- Limit: 5 requests per 60 seconds per IP (identified via the
CF-Connecting-IPheader). - Requests over the limit receive
429 Too Many Requests.
- Each markdown file in
notes/is the source of truth for one RAG note. - The filename must follow this format:
${id}_${name_for_human_editor}.md
Examples:
1_chiens_work_experience.md
2_projects.md
15_publications.md
${id}must be a positive integer.- The sync script normalizes the filename ID before sending it to the API.
- The resulting ID is reused as the note ID in D1 and Vectorize.
- Remote note writes happen through
PUT /notes/:id(local only) with a JSON body shaped like{ "text": "..." }. In production, only/chatbotrequests reach the Worker, so/notes/:idis unreachable at the edge. - Renaming or deleting local files affects remote data on the next sync.
- Create or edit markdown files in
notes/. - Copy
.env.local.exampleto.env.local(first time only):
cp .env.local.example .env.local- Start the local Worker:
npx wrangler dev- In another terminal, run the sync:
npm run sync:notesThe workflow may take a moment to process. Monitor the npx wrangler dev output to confirm the upload has completed.
The sync script is scripts/sync-rag-notes.sh.
It will:
- Scan
notes/for*.mdfiles. - Validate that every filename matches
${id}_*.md. - Compute a SHA-256 hash for each file.
- Upload only notes whose content changed since the last successful sync.
- Delete remote notes whose IDs existed in the previous sync state but no longer exist locally.
- Save sync state in
.rag-sync-state.
Because of this behavior:
- Editing a file updates the remote note
- Adding a file creates a remote note
- Deleting a file deletes the remote note
- Changing only the human-readable suffix keeps the same remote note if the numeric ID stays the same
Missing env variable: ensure.env.localexists and containsADMIN_API_ENABLED=true.Notes directory not found: createnotes/or setRAG_SYNC_NOTES_DIR.Invalid note filename: rename the file to${id}_name.md.Duplicate note id: ensure only one markdown file uses each numeric ID.
wrangler dev in this project is configured with remote Cloudflare bindings. A live sync updates the bound D1 and Vectorize resources, so use the sync flow carefully.