Visitar URL original
Cloudflare Worker Full Example | ChatCops Skip to content

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.

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.

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:

Terminal window
npx wrangler d1 create chatcops
Terminal window
npx wrangler secret put ANTHROPIC_API_KEY
npx wrangler secret put CHATCOPS_SESSION_SECRET # openssl rand -base64 32
npx wrangler secret put WEBHOOK_URL
Terminal window
npx wrangler deploy
<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>
Terminal window
npm install @chatcops/server @chatcops/core @chatcops/store-cloudflare

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.

retention sets how long conversations are kept, and something has to call purge(). See running purge() on a schedule for a cron trigger.