Cloudflare Worker Full Example
A production-ready Cloudflare Worker with lead capture, a knowledge base, rate limits and budgets. Conversations live in D1 and the counters in a Durable Object, so every isolate and every region sees the same history and the same limits.
Worker (src/index.ts)
Section titled “Worker (src/index.ts)”import { chatcopsCloudflareHandler } from '@chatcops/server';import { FAQKnowledgeSource, LeadCaptureTool, TextKnowledgeSource, type WorkersAiBinding,} from '@chatcops/core';import { cloudflareStorage, ChatCopsCounter } from '@chatcops/store-cloudflare';
// Cloudflare finds the Durable Object class through the Worker's exports.export { ChatCopsCounter };
interface Env { DB: D1Database; COUNTERS: DurableObjectNamespace<ChatCopsCounter>; AI: WorkersAiBinding; ANTHROPIC_API_KEY: string; CHATCOPS_SESSION_SECRET: string; WEBHOOK_URL: string; SYSTEM_PROMPT: string;}
// Built on the first request rather than at import, because the API key and// the bindings only exist inside a request.let handler: ReturnType<typeof chatcopsCloudflareHandler> | undefined;function getHandler(env: Env) { handler ??= chatcopsCloudflareHandler({ provider: { type: 'claude', apiKey: env.ANTHROPIC_API_KEY, model: 'claude-haiku-5-5', },
systemPrompt: env.SYSTEM_PROMPT || `You are a helpful customer support assistant. Be friendly and concise. Capture leads when users share contact info.`,
tools: [ new LeadCaptureTool(async (lead) => { // Forward to webhook if (env.WEBHOOK_URL) { await fetch(env.WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'lead', ...lead }), }); } }), ],
knowledge: [ new FAQKnowledgeSource([ { question: 'Pricing?', answer: 'Free tier available. Pro starts at $29/mo.' }, { question: 'Support hours?', answer: '24/7 via chat, email support 9am-5pm EST.' }, { question: 'Refund policy?', answer: '30-day money-back guarantee on all plans.' }, ]), new TextKnowledgeSource([ `We are a SaaS company specializing in developer tools. Founded in 2023. Based in San Francisco. Used by 1000+ developers worldwide.`, ]), ],
storage: cloudflareStorage({ d1: env.DB, counters: env.COUNTERS }), retention: { conversationsDays: 30 }, allowedOrigins: ['https://your-site.com'], sessionSecrets: [env.CHATCOPS_SESSION_SECRET], rateLimit: { maxRequests: 20, windowMs: 60_000 }, budgets: { globalPerDay: { tokens: 20_000_000 } }, analytics: true, }); return handler;}
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { // Health check if (new URL(request.url).pathname === '/health') { return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' }, }); }
// ctx lets lead webhooks and analytics writes finish after the reply. return getHandler(env)(request, env, ctx); },};To query a persistent vector store (such as Cloudflare Vectorize) alongside static knowledge sources, connect workersAiEmbedder({ ai: env.AI }) to a vector retriever and add workersAiReranker({ ai: env.AI }) to rerank its candidates with @cf/baai/bge-reranker-base:
import { hybridRetriever, retrieverKnowledge, workersAiEmbedder, workersAiReranker, type VectorStore, type WorkersAiBinding } from '@chatcops/core';
declare const env: { AI: WorkersAiBinding };declare const vectors: VectorStore;
const embedder = workersAiEmbedder({ ai: env.AI });const retriever = hybridRetriever({ store: vectors, embedder, reranker: workersAiReranker({ ai: env.AI }),});const knowledge = retrieverKnowledge(retriever, { topK: 5 });See Retrieval from a vector store for setup and retriever options. The Workers AI reranker model accepts the query and candidate contexts and returns their relevance scores.
wrangler.toml
Section titled “wrangler.toml”name = "chatcops-worker"main = "src/index.ts"compatibility_date = "2026-10-01"compatibility_flags = ["nodejs_compat"]
[vars]SYSTEM_PROMPT = "You are a helpful AI assistant."
[ai]binding = "AI"
[[d1_databases]]binding = "DB"database_name = "chatcops"database_id = "<from wrangler d1 create>"
[[durable_objects.bindings]]name = "COUNTERS"class_name = "ChatCopsCounter"
[[migrations]]tag = "v1"new_sqlite_classes = ["ChatCopsCounter"]Create the database once. The store creates its tables on first use:
npx wrangler d1 create chatcopsSecrets
Section titled “Secrets”npx wrangler secret put ANTHROPIC_API_KEYnpx wrangler secret put CHATCOPS_SESSION_SECRET # openssl rand -base64 32npx wrangler secret put WEBHOOK_URLDeploy
Section titled “Deploy”npx wrangler deployWidget HTML
Section titled “Widget HTML”<script src="https://cdn.jsdelivr.net/npm/@chatcops/widget@1/dist/chatcops.min.js" data-api-url="https://chatcops-worker.your-subdomain.workers.dev" data-accent="#6366f1" data-brand-name="AI Support" data-welcome-message="Hi! How can I help?"></script>Install
Section titled “Install”npm install @chatcops/server @chatcops/core @chatcops/store-cloudflareWithout a Durable Object
Section titled “Without a Durable Object”Leave out counters and the COUNTERS binding, and the counters go to D1 as well. Limits still hold across isolates, since each count is one atomic D1 statement, but every count is a D1 write. Turn locks always live in D1.
Cleaning up old conversations
Section titled “Cleaning up old conversations”retention sets how long conversations are kept, and something has to call purge(). See running purge() on a schedule for a cron trigger.