Show HN:DocFlare AI——可在 Cloudflare 免费套餐上运行的开源文档聊天机器人
英文标题:🔶 Show HN: DocFlare AI – Open-source docs chatbot on Cloudflare's free tier
DocFlare AI 是一个完全基于 Cloudflare 原生服务构建的开源文档搜索与 RAG 聊天机器人,只需在文档站点加入一段 `<script>` 即可嵌入。项目整合 Workers、Workers AI、Vectorize、D1、Cron Triggers 和 Static Assets,在免费套餐额度内提供爬取、向量检索、重排序、流式回答与聊天记录功能。它无需 OpenAI API Key、Pinecone、独立服务器或付费托管服务,并支持多站点隔离与内置管理后台。对开发者而言,该项目提供了一套可自行部署、可定制的“与文档聊天”基础设施,替代每月收费 20 至 400 美元的托管产品。
1.1 KB
344.0 KB
350.3 KB
中文
开源、零成本的 AI 网站搜索与聊天机器人组件。100% Cloudflare 原生。
在你的文档站点加入一个 <script> 标签,即可获得一个可从你的内容中检索答案、附带来源且支持流式输出的 RAG 聊天机器人。无需 OpenAI Key。无需 Pinecone。无需服务器。无需账单。
| 嵌入文档站点的聊天组件 | 管理 UI(/admin) |
|---|---|
![]() |
![]() |
左侧为带来源标签的流式回答;右侧为内置管理页面,可用于注册站点地图、执行索引以及查看用户提问。
托管式“与你的文档聊天”组件(Chatbase、CustomGPT、Mendable 等)每月收费 20 至 400 美元;其底层实际上就是爬虫、嵌入模型、向量存储和 LLM。Cloudflare 现在为这些组成部分全部提供了额度充足的免费套餐:
| 组件 | Cloudflare 服务 | 免费套餐(撰写时) |
|---|---|---|
| API / 爬虫 | Workers + Hono | 每天 10 万次请求 |
| 嵌入 + LLM | Workers AI(bge-small-en-v1.5、bge-reranker-base、llama-3.1-8b-instruct-fp8) |
每天 1 万 neurons |
| 向量搜索 | Vectorize(384 维、余弦相似度) | 存储 500 万维度、每月查询 3000 万维度 |
| 元数据 + 聊天日志 | D1(SQLite) | 5 GB、每天 500 万次读取 |
| 后台索引 | Cron Triggers | 已包含 |
| 组件托管 | Static Assets | 已包含 |
DocFlare AI 将它们连接为单个 Worker,只需一条命令即可部署。
-
单标签嵌入:
<script src=".../widget.js" data-site-id="my-docs">。原生 JavaScript、Shadow DOM、小于 10 KB、零依赖、不会造成 CSS 泄漏。 -
通过 Server-Sent Events 提供流式回答,包含来源标签,并使用提问所用的语言回答。
-
站点地图爬虫:支持 sitemap 索引、gzip sitemap,并通过原生
HTMLRewriter去除样板内容(导航、页头、页脚、脚本)。 -
感知章节的分块:标题会开启新的块,因此着陆页的“开源”卡片不会被埋在有关定价的内容块里。每个块约为 350 个 token,重叠 40 个 token;通过按脚本计算 token 估值,使越南语/CJK 页面保持在嵌入模型 512-token 的窗口内。
-
两阶段检索:20 个最佳向量匹配结果会先由
bge-reranker-base交叉编码器重新评分,之后再将 top-K 结果送入 LLM。仅与错误页面共享词汇的问题(例如法律页面中的站点名、产品名),会被定位到真正回答问题的段落。 -
多站点 / 多租户:一次部署可服务多个文档站点,每个站点均隔离在各自的 Vectorize namespace 中,并锁定至其专属域名。
-
设计上适配免费套餐:索引以可恢复的小批次运行(cron + 按需调用);未检索到相关内容时跳过 LLM;AI 速率限制会优雅降级。
-
深色 / 浅色 / 自动主题,支持自定义强调色、左/右位置,并对键盘操作友好。
-
将聊天记录保存到 D1,让你了解用户提出的问题,以及文档存在哪些缺口。
-
位于
/admin的内置管理 UI:注册站点地图、查看索引进度、处理批次、重新爬取、删除站点、复制嵌入代码片段,以及浏览最近问题。单个静态 HTML 文件,无需构建步骤。 -
端到端 TypeScript、Hono 路由器、严格类型、没有
any。使用 Bun 进行工具链和测试。flowchart LR subgraph Host["Your docs site"] W["widget.js(Shadow DOM)"] end
subgraph CF["Cloudflare (free tier)"] direction TB API["Worker + Hono<br/>src/index.ts"] AI["Workers AI<br/>bge-small-en-v1.5<br/>bge-reranker-base<br/>llama-3.1-8b-instruct-fp8"] VX["Vectorize<br/>384-dim cosine<br/>namespace = siteId"] D1["D1<br/>sites / pages / queries"] CRON["Cron trigger<br/>every minute"] ASSETS["Static assets<br/>public/"] end SM["sitemap.xml + HTML pages"] W -- "POST /api/chat (SSE)" --> API ASSETS -- "GET /widget.js" --> W API -- "embed question" --> AI API -- "topK query" --> VX API -- "RAG prompt" --> AI API -- "log query" --> D1 CRON -- "process pending pages" --> API API -- "fetch + extract" --> SM API -- "embed chunks" --> AI API -- "upsert vectors" --> VX
加载中
索引:POST /api/sites/index 读取站点地图,并将每个 HTML URL 加入 D1 队列。随后,cron trigger(或重复调用 /process)会在每次调用中爬取少量页面:提取文本、分块、嵌入、写入 Vectorize,并将页面标记为已索引。
聊天:组件发送问题;Worker 对问题进行嵌入,从站点 namespace 中取回最接近的 20 个内容块,通过交叉编码器对它们重排序,保留 top-K(每页最多 2 个),构建基于检索内容的提示词,以 SSE 流式返回 Llama 3.1 的回答,并将对话记录到 D1。
完整设计请参阅 docs/ARCHITECTURE.md,端点说明请参阅 docs/API_REFERENCE.md。
-
一个启用了 Workers 的免费 Cloudflare 账户。
-
Bun 1.1+(用于安装、脚本、构建组件和运行测试)。
git clone https://github.com/p10node/docflare-ai.git cd docflare-ai bun install bunx wrangler login
2. 创建 D1 数据库
bunx wrangler d1 create docflare-db
从输出中复制 database_id。你可以将其粘贴到 wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "docflare-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # <- paste here
……或者,为避免将它写入 git,可将其改为放入 .env 文件中(当你推送 fork 时很有用):
echo 'D1_DATABASE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' > .env
设置 .env 后,bun run deploy 和 bun run db:migrate 会从 wrangler.toml 生成已填入 ID 的 wrangler.local.toml(已被 gitignore),并使用该文件。wrangler.toml 将保留占位符。
应用 schema:
3. 创建 Vectorize 索引
bunx wrangler vectorize create docflare-index --dimensions=384 --metric=cosine
(384 维度与 @cf/baai/bge-small-en-v1.5 匹配。索引名称已在 wrangler.toml 中设置。)
管理端点由 bearer token 保护。请选择一个足够长的随机值:
bunx wrangler secret put ADMIN_TOKEN
bun run deploy 会构建组件并部署(schema 已在第 2 步应用;每当 migrations/ 增加新文件时,请重新运行 bun run db:migrate)。Wrangler 会打印你的 Worker URL,例如 https://docflare-ai.<your-subdomain>.workers.dev。打开该地址即可查看已挂载组件的演示页面。
部署到 Cloudflare 按钮:本 README 顶部的按钮会将仓库克隆到你的 GitHub 账户,根据
wrangler.toml配置创建 Worker、D1 数据库和 Vectorize 索引,要求你提供ADMIN_TOKEN(来自.dev.vars.example),并运行bun run deploy。向导无法从wrangler.toml读取以下两个值:
- Vectorize 索引:请输入 Dimensions = 384 和 Metric = cosine(表单默认留空;任何其他规格都会导致索引失败)。
ADMIN_TOKEN:请将示例值替换为长且随机的值,例如openssl rand -hex 32。
构建过程不会应用 D1 schema(构建 token 没有 D1 访问权限),因此请在首次部署后从新仓库运行一次迁移。Cloudflare 在创建资源时会将你的
database_id写入该仓库的wrangler.toml,因此不需要.env(如果仍显示占位符,请执行第 2 步):git clone https://github.com/<you>/docflare-ai.git && cd docflare-ai bun install && bunx wrangler login bun run db:migrate bunx wrangler vectorize get docflare-index # expect dimensions: 384, metric: cosine如果索引缺失或规格不同,请执行第 3 步(先通过
bunx wrangler vectorize delete docflare-index删除它)后重新部署。如果向导未要求提供ADMIN_TOKEN,请执行第 4 步。
export WORKER_URL="https://docflare-ai.<your-subdomain>.workers.dev"
export ADMIN_TOKEN="the-token-you-set"
curl -X POST "$WORKER_URL/api/sites/index" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"siteId":"my-docs","sitemapUrl":"https://docs.example.com/sitemap.xml"}'
响应:
{
"siteId": "my-docs",
"domain": "docs.example.com",
"sitemapUrl": "https://docs.example.com/sitemap.xml",
"queued": 142,
"progress": { "siteId": "my-docs", "total": 142, "pending": 142, "indexed": 0, "failed": 0 },
"next": "Pending pages are crawled 3 at a time by the cron trigger. Call POST /api/sites/my-docs/process repeatedly to finish faster."
}
更喜欢 UI?打开 https://docflare-ai.<your-subdomain>.workers.dev/admin,粘贴管理员 token,并使用 Register / re-index a site 表单。同一页面还会显示每个站点的进度条、一个循环调用 /process 直至队列清空的 Index all pending 按钮,以及最近问题日志。
现在,cron trigger 会自动每分钟爬取少量页面。若要更快完成,可从 CLI 自行驱动队列:
until curl -sf -X POST "$WORKER_URL/api/sites/my-docs/process" \
-H "Authorization: Bearer $ADMIN_TOKEN" | grep -q '"pending":0'; do
sleep 1
done
可随时检查进度:
curl "$WORKER_URL/api/sites/my-docs/status" -H "Authorization: Bearer $ADMIN_TOKEN"
针对相同 siteId 重新运行 /api/sites/index 会将所有页面重新加入队列,因此可通过 cron job 或 CI 步骤保持索引最新。Vectorize 为最终一致性系统:新写入的内容块将在数秒内可被搜索。
在升级到会改变分块逻辑的 DocFlare 版本后(请参见提交信息中的 changelog),请对每个站点重新索引一次(管理 UI 中选择 Re-crawl sitemap,或调用
POST /api/sites/index),以便重新分块页面。在此之前,旧向量仍可正常工作。
在已注册域名的任意页面中,于 </body> 前添加一行:
<script src="https://docflare-ai.<your-subdomain>.workers.dev/widget.js"
data-site-id="my-docs"
defer></script>
| 属性 | 默认值 | 说明 |
|---|---|---|
data-site-id |
必填 | 你已索引的 siteId。 |
data-api |
脚本源站 | 如果从其他位置(例如 CDN)提供 widget.js,则设置 Worker 的基础 URL。 |
data-theme |
auto |
light、dark 或 auto(遵循 prefers-color-scheme)。 |
data-color |
#f6821f |
气泡和按钮的强调色。 |
data-position |
right |
right 或 left 角落。 |
data-title |
Ask AI |
面板标题文字。 |
data-placeholder |
Ask a question about these docs… |
输入框占位文本。 |
data-welcome |
Hi! Ask me anything… |
助手的第一条消息。 |
一个轻量 JavaScript API 通过 window.DocFlare 暴露:
DocFlare.open(); // open the panel
DocFlare.close();
DocFlare.ask('How do I deploy?'); // open and submit a question
只有从该站点已注册 domain(以及 ALLOWED_ORIGINS 中的任何源)提供的页面,才能为该 siteId 调用 /api/chat。来自其他源的请求将得到 403。
非敏感配置位于 wrangler.toml 的 [vars] 部分:
| 变量 | 默认值 | 用途 |
|---|---|---|
MAX_PAGES |
200 |
每次索引运行从站点地图中获取的最大 URL 数。 |
INDEX_BATCH_SIZE |
3 |
每个 cron tick 或 /process 调用中爬取并嵌入的页面数。使用 Workers Paid 套餐时可提高至 10-20。 |
TOP_K |
4 |
每个问题交给 LLM 的内容块数量(最多 20)。 |
RERANK |
true |
在选取 TOP_K 前,使用 @cf/baai/bge-reranker-base 对 20 个最佳向量匹配进行重新排序。false = 仅使用向量排序。 |
ALLOWED_ORIGINS |
"" |
允许为任意站点调用 /api/chat 的额外源,以逗号分隔。* 表示允许所有来源。 |
密钥(通过 bunx wrangler secret put NAME 设置):
| 密钥 | 用途 |
|---|---|
ADMIN_TOKEN |
/api/sites/* 的 Bearer token。必填;在设置前,管理路由将返回 503。 |
想使用更大的模型或不同的嵌入维度?请修改 src/services/ai.ts 中的 CHAT_MODEL / EMBEDDING_MODEL,并创建维度匹配的 Vectorize 索引。
cp .dev.vars.example .dev.vars # sets ADMIN_TOKEN for local runs
bunx wrangler d1 migrations apply docflare-db --local
bun run dev # http://localhost:8787
注意:
- Workers AI 和 Vectorize
English / 原文
Open-source, zero-cost AI site search & chatbot widget. 100% Cloudflare native.
Drop one <script> tag on your docs site and get a streaming RAG chatbot that answers from your content, with sources. No OpenAI key. No Pinecone. No servers. No bill.
| Chat widget embedded in a docs site | Admin UI (/admin) |
|---|---|
![]() |
![]() |
Streaming answers with source chips on the left; the built-in admin page for registering sitemaps, driving indexing and reviewing what users ask on the right.
Hosted "chat with your docs" widgets (Chatbase, CustomGPT, Mendable, ...) charge $20-$400/month for something that is, under the hood, a crawler, an embedding model, a vector store and an LLM. Cloudflare now ships every one of those pieces with a generous free tier:
| Piece | Cloudflare service | Free tier (at time of writing) |
|---|---|---|
| API / crawler | Workers + Hono | 100k requests/day |
| Embeddings + LLM | Workers AI (bge-small-en-v1.5, bge-reranker-base, llama-3.1-8b-instruct-fp8) |
10k neurons/day |
| Vector search | Vectorize (384-dim, cosine) | 5M stored dims, 30M queried dims/month |
| Metadata + chat log | D1 (SQLite) | 5 GB, 5M reads/day |
| Background indexing | Cron Triggers | included |
| Widget hosting | Static Assets | included |
DocFlare AI wires them together into a single Worker you deploy with one command.
-
One-tag embed :
<script src=".../widget.js" data-site-id="my-docs">. Vanilla JS, Shadow DOM, < 10 KB, zero dependencies, no CSS leaks. -
Streaming answers over Server-Sent Events with source chips, in the language of the question.
-
Sitemap crawler : sitemap index support, gzip sitemaps, boilerplate stripping (nav/header/footer/scripts) via the native
HTMLRewriter. -
Section-aware chunking : headings start new chunks, so a landing page's "Open source" card is not buried in a chunk about pricing. Chunks are ~350 tokens with 40-token overlap, sized with a per-script token estimate so Vietnamese/CJK pages stay inside the embedding model's 512-token window.
-
Two-stage retrieval : the 20 best vector matches are re-scored by the
bge-reranker-basecross-encoder before the top-K go to the LLM. Questions that merely share vocabulary with the wrong page (the site name on legal pages, a product name) land on the paragraph that actually answers them. -
Multi-site / multi-tenant : one deployment can serve many documentation sites, each isolated in its own Vectorize namespace and locked to its own domain.
-
Free-tier safe by design : indexing runs in small resumable batches (cron + on-demand), the LLM is skipped when nothing relevant is retrieved, and AI rate limits degrade gracefully.
-
Dark / light / auto theme , custom accent colour, left/right position, keyboard friendly.
-
Chat history in D1 so you can see what users ask and where your docs have gaps.
-
Built-in admin UI at
/admin: register sitemaps, watch indexing progress, process batches, re-crawl, delete sites, copy the embed snippet and browse recent questions. Single static HTML file, no build step. -
TypeScript end-to-end, Hono router, strict types, no
any. Bun for tooling and tests.flowchart LR subgraph Host["Your docs site"] W["widget.js(Shadow DOM)"] end
subgraph CF["Cloudflare (free tier)"] direction TB API["Worker + Hono<br/>src/index.ts"] AI["Workers AI<br/>bge-small-en-v1.5<br/>bge-reranker-base<br/>llama-3.1-8b-instruct-fp8"] VX["Vectorize<br/>384-dim cosine<br/>namespace = siteId"] D1["D1<br/>sites / pages / queries"] CRON["Cron trigger<br/>every minute"] ASSETS["Static assets<br/>public/"] end SM["sitemap.xml + HTML pages"] W -- "POST /api/chat (SSE)" --> API ASSETS -- "GET /widget.js" --> W API -- "embed question" --> AI API -- "topK query" --> VX API -- "RAG prompt" --> AI API -- "log query" --> D1 CRON -- "process pending pages" --> API API -- "fetch + extract" --> SM API -- "embed chunks" --> AI API -- "upsert vectors" --> VX
Loading
Indexing : POST /api/sites/index reads the sitemap and queues every HTML URL in D1. A cron trigger (or repeated calls to /process) then crawls a few pages per invocation: extract text, chunk, embed, upsert to Vectorize, mark the page indexed.
Chat : the widget sends the question; the Worker embeds it, pulls the 20 closest chunks from the site's namespace, reranks them with a cross-encoder, keeps the top-K (max 2 per page), builds a grounded prompt, streams Llama 3.1's answer back as SSE and logs the exchange in D1.
See docs/ARCHITECTURE.md for the full design and docs/API_REFERENCE.md for the endpoints.
-
A free Cloudflare account with Workers enabled.
-
Bun 1.1+ (used for installing, scripts, building the widget and running tests).
git clone https://github.com/p10node/docflare-ai.git cd docflare-ai bun install bunx wrangler login
2. Create the D1 database
bunx wrangler d1 create docflare-db
Copy the database_id from the output. Either paste it into wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "docflare-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # <- paste here
…or, to keep it out of git, put it in a .env file instead (useful when you push your fork):
echo 'D1_DATABASE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' > .env
With .env set, bun run deploy and bun run db:migrate generate wrangler.local.toml (gitignored) from wrangler.toml with the id filled in and use that. wrangler.toml keeps the placeholder.
Apply the schema:
3. Create the Vectorize index
bunx wrangler vectorize create docflare-index --dimensions=384 --metric=cosine
(384 dimensions matches @cf/baai/bge-small-en-v1.5. The index name is already set in wrangler.toml.)
The management endpoints are protected by a bearer token. Pick something long and random:
bunx wrangler secret put ADMIN_TOKEN
bun run deploy builds the widget and deploys (the schema was applied in step 2; re-run bun run db:migrate whenever migrations/ gains a new file). Wrangler prints your Worker URL, e.g. https://docflare-ai.<your-subdomain>.workers.dev. Open it to see the demo page with the widget mounted.
Deploy to Cloudflare button : the button at the top of this README clones the repo into your GitHub account, provisions the Worker, D1 database and Vectorize index from
wrangler.toml, asks forADMIN_TOKEN(from.dev.vars.example) and runsbun run deploy. Two values the wizard cannot read fromwrangler.toml:
- Vectorize index : enter Dimensions = 384 and Metric = cosine (the form leaves them blank; any other shape breaks indexing).
ADMIN_TOKEN: replace the example value with something long and random, e.g.openssl rand -hex 32.
The build does not apply the D1 schema (the build token has no D1 access), so run the migration once from your new repo after the first deploy. Cloudflare writes your
database_idinto that repo'swrangler.tomlwhile provisioning, so no.envis needed (if it still shows the placeholder, do step 2):git clone https://github.com/<you>/docflare-ai.git && cd docflare-ai bun install && bunx wrangler login bun run db:migrate bunx wrangler vectorize get docflare-index # expect dimensions: 384, metric: cosineIf the index is missing or has a different shape, run step 3 (delete it first with
bunx wrangler vectorize delete docflare-index) and redeploy. If the wizard did not ask forADMIN_TOKEN, run step 4.
export WORKER_URL="https://docflare-ai.<your-subdomain>.workers.dev"
export ADMIN_TOKEN="the-token-you-set"
curl -X POST "$WORKER_URL/api/sites/index" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"siteId":"my-docs","sitemapUrl":"https://docs.example.com/sitemap.xml"}'
Response:
{
"siteId": "my-docs",
"domain": "docs.example.com",
"sitemapUrl": "https://docs.example.com/sitemap.xml",
"queued": 142,
"progress": { "siteId": "my-docs", "total": 142, "pending": 142, "indexed": 0, "failed": 0 },
"next": "Pending pages are crawled 3 at a time by the cron trigger. Call POST /api/sites/my-docs/process repeatedly to finish faster."
}
Prefer a UI? Open https://docflare-ai.<your-subdomain>.workers.dev/admin, paste the admin token and use the Register / re-index a site form. The same page shows progress bars per site, an Index all pending button that loops over /process until the queue is empty, and the recent questions log.
Pages are now crawled automatically, a few per minute, by the cron trigger. To finish faster, drive the queue yourself from the CLI:
until curl -sf -X POST "$WORKER_URL/api/sites/my-docs/process" \
-H "Authorization: Bearer $ADMIN_TOKEN" | grep -q '"pending":0'; do
sleep 1
done
Check progress at any time:
curl "$WORKER_URL/api/sites/my-docs/status" -H "Authorization: Bearer $ADMIN_TOKEN"
Re-running /api/sites/index for the same siteId re-queues every page, so a cron job or CI step can keep the index fresh. Vectorize is eventually consistent: freshly upserted chunks become searchable within a few seconds.
After upgrading DocFlare to a version that changes chunking (see the changelog in commit messages), re-index every site once (Re-crawl sitemap in the admin UI or
POST /api/sites/index) so pages are re-chunked. Old vectors keep working until then.
Add one line before </body> on any page of the registered domain:
<script src="https://docflare-ai.<your-subdomain>.workers.dev/widget.js"
data-site-id="my-docs"
defer></script>
| Attribute | Default | Description |
|---|---|---|
data-site-id |
required | The siteId you indexed. |
data-api |
script origin | Base URL of the Worker, if you serve widget.js from elsewhere (e.g. a CDN). |
data-theme |
auto |
light, dark or auto (follows prefers-color-scheme). |
data-color |
#f6821f |
Accent colour for the bubble and buttons. |
data-position |
right |
right or left corner. |
data-title |
Ask AI |
Panel header text. |
data-placeholder |
Ask a question about these docs… |
Input placeholder. |
data-welcome |
Hi! Ask me anything… |
First assistant message. |
A tiny JavaScript API is exposed as window.DocFlare:
DocFlare.open(); // open the panel
DocFlare.close();
DocFlare.ask('How do I deploy?'); // open and submit a question
Only pages served from the site's registered domain (and anything in ALLOWED_ORIGINS) can call /api/chat for that siteId. Requests from other origins get a 403.
Non-secret settings live in the [vars] section of wrangler.toml:
| Variable | Default | Purpose |
|---|---|---|
MAX_PAGES |
200 |
Max URLs taken from a sitemap per indexing run. |
INDEX_BATCH_SIZE |
3 |
Pages crawled + embedded per cron tick or /process call. Raise to 10-20 on the Workers Paid plan. |
TOP_K |
4 |
Chunks handed to the LLM per question (max 20). |
RERANK |
true |
Rerank the 20 best vector matches with @cf/baai/bge-reranker-base before picking TOP_K. false = vector order only. |
ALLOWED_ORIGINS |
"" |
Comma-separated extra origins allowed to call /api/chat for any site. * allows everything. |
Secrets (set with bunx wrangler secret put NAME):
| Secret | Purpose |
|---|---|
ADMIN_TOKEN |
Bearer token for /api/sites/*. Required; management routes return 503 until it is set. |
Want a bigger model or a different embedding size? Change CHAT_MODEL / EMBEDDING_MODEL in src/services/ai.ts and create a Vectorize index with the matching dimensions.
cp .dev.vars.example .dev.vars # sets ADMIN_TOKEN for local runs
bunx wrangler d1 migrations apply docflare-db --local
bun run dev # http://localhost:8787
Notes:
- Workers AI and Vectorize have no local emulator;
wrangler devproxies those bindings to your Cloudflare account (you must be logged in). D1 runs locally. wrangler devuses a local D1 and ignoresdatabase_id, so neither the real id inwrangler.tomlnor.envis needed for local runs. Onlybun run deployandbun run db:migrateneed it.- The Vectorize index must exist remotely before
wrangler devcan use it. - Test the cron handler locally with
bun run dev:cron, thencurl "http://localhost:8787/__scheduled?cron=*+*+*+*+*". bun testruns the unit tests (token estimate, section chunker, HTML extraction, sitemap filtering, CORS policy, reranker, prompt builder).bun run build:checktype-checks and performs a dry-run bundle without deploying.bun run build:widgetminifieswidget/widget.src.jsintopublic/widget.jswithbun build(run after editing the widget).
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/health |
none | Liveness + model names. |
POST |
/api/sites/index |
admin | Register a site and queue its sitemap URLs. |
POST |
/api/sites/:siteId/process |
admin | Crawl and embed the next batch of pending pages. |
GET |
/api/sites/:siteId/status |
admin | Indexing progress. |
DELETE |
/api/sites/:siteId |
admin | Remove a site, its pages, chat log and vectors. |
POST |
/api/chat |
origin check | Ask a question (JSON or SSE stream). |
GET |
/widget.js |
none | Embeddable widget. |
Full request/response shapes, the SSE protocol and an OpenAPI document are in docs/API_REFERENCE.md.
Staying inside the free tier
Rough budget for a typical documentation site, based on Cloudflare pricing as of 2026-10-01 (check the Workers AI, Vectorize, D1 and Workers pricing pages for up-to-date numbers):
- Indexing :
bge-smallcosts 1,841 neurons per million input tokens, i.e. roughly 1 neuron per page. A 1,000-page site fits comfortably in a day's free quota. - Chat : one answered question costs roughly 30-70 neurons : the prompt is 1.5k-3.5k tokens (system prompt +
TOP_K=4chunks of ~350 tokens + conversation history) at 13,778 neurons/M, the answer is 300-768 tokens at 26,128 neurons/M, the query embedding is negligible and reranking 20 candidates (~6k tokens at 283 neurons/M) costs about 2 neurons. With 10k free neurons/day that is on the order of 120-280 answered questions per day for free. Questions with no relevant context never hit the LLM and cost ~0. - Vectorize : 5M free stored dimensions / 384 ≈ 13,000 chunks ≈ 1,000-2,500 pages across all sites (section-aware chunks are smaller and more numerous than fixed 500-token blocks). Each query reads 384 dimensions, so even 2,500 questions/day stay under the 30M queried dimensions/month allowance.
- D1 and Workers requests : one chat is 1 row read + 1 row written; the per-minute cron adds ~1.4k reads/day. Both are orders of magnitude below the free limits.
- CPU time : the Workers Free plan allows 10 ms CPU per invocation. Crawling is I/O bound and
HTMLRewriteris native, so a batch of 3 pages stays under the limit; on the Paid plan raiseINDEX_BATCH_SIZE.
Workers AI is the only piece you will outgrow. Past the daily neuron allowance the AI binding fails and /api/chat returns 429 rate_limited until the quota resets. To go further, move to the Workers Paid plan ($5/month) : it keeps the 10k free neurons/day and bills $0.011 per 1,000 neurons beyond that. No code changes are needed.
| Questions / day | Plan | Workers AI | Total / month (approx.) |
|---|---|---|---|
| up to ~280 | Free | $0 | $0 |
| 1,000 | Paid | $9-24 | $14-29 |
| 5,000 | Paid | $56-132 | $61-137 |
That is about $0.0004-0.0009 per question beyond the free allowance. The spread comes from how long the retrieved chunks, the history and the answers are.
To cut the cost per question:
-
Lower
TOP_Kfrom 4 to 3 (about 25% fewer prompt tokens, slightly less context for the model). -
Lower
MAX_OUTPUT_TOKENSinsrc/services/ai.ts(768 by default); output tokens are the most expensive kind. -
Trim
MAX_HISTORY_TURNS/MAX_HISTORY_CHARSinsrc/services/ai.tsif follow-up questions are rare on your site. -
Swap
CHAT_MODELfor a cheaper model from the Workers AI catalog; the response parser accepts both Llama-style and OpenAI-style outputs. -
English-optimised embedding model (
bge-small-en). For multilingual docs switch to@cf/baai/bge-m3(1024 dims) and recreate the index. -
JavaScript-rendered sites must expose server-rendered HTML (most docs generators do).
-
No authentication for private docs yet (the widget is meant for public sites).
-
Planned: incremental re-crawl using sitemap
<lastmod>, feedback thumbs on answers, Markdown/llms.txt ingestion, per-site widget settings in the admin UI.docflare-ai/ ├── docs/ │ ├── images/ # README screenshots │ ├── ARCHITECTURE.md # System design & RAG data flow │ └── API_REFERENCE.md # REST + SSE endpoint reference, OpenAPI ├── migrations/ │ └── 0000_init.sql # D1 schema ├── scripts/ │ └── wrangler.ts # Wrapper: swaps D1_DATABASE_ID from .env (optional) into a gitignored wrangler.local.toml ├── public/ │ ├── _headers # Static asset headers (CORS / caching for widget.js) │ ├── admin.html # Admin UI (served at /admin) │ ├── index.html # Demo page │ └── widget.js # Minified embeddable widget (built from widget/) ├── widget/ │ └── widget.src.js # Readable widget source (bun build → public/widget.js) ├── test/ │ └── unit.test.ts # bun test: chunker, crawler filters, CORS, prompt ├── src/ │ ├── db/schema.ts # D1 statements, row types, repository │ ├── services/ │ │ ├── ai.ts # Workers AI embeddings, RAG prompt, generation │ │ ├── crawler.ts # Sitemap fetcher, HTMLRewriter text extractor, chunker │ │ ├── indexer.ts # Batch indexing pipeline │ │ └── vectorize.ts # Vectorize upsert / query / delete │ ├── utils/ │ │ ├── cors.ts # Multi-domain CORS + origin policy │ │ └── hash.ts # SHA-256 ids, constant-time compare │ ├── index.ts # Hono app, routes, cron handler │ └── types.ts # Env bindings & shared types ├── wrangler.toml ├── .dev.vars.example # ADMIN_TOKEN etc. for
wrangler dev├── package.json ├── bun.lock └── tsconfig.json
Issues and pull requests are welcome. Run bun test and bun run build:check before opening a PR; they run the unit tests, type-check and dry-run bundle the Worker.

