Have you ever wanted to add an intercom or enhanced, AI-powered chatbot to your website, without paying a high monthly fee? In this tutorial, you’ll learn how to build a fully functional, embeddable AI chatbot widget using CloudFlyer’s serverless stack.
You’ll create a production-ready AI chatbot widget that you can embed on any website with any script tag. It’ll be like Intercom or Enhanced – but it’s completely free and under your control.
In the end, you’ll have a chatbot that:
Drives real-time AI responses for a natural typing effect
Answer questions from your FAQ using Raga (Recovery Augmented Generation).
Remembers conversations during page reloads
Supports dark and light modes
Works on any website with one line of code
Table of Contents
Conditions
Before you begin, make sure you have:
a CloudFlyer account (free tier works perfectly)
node.js Version 18 or higher is installed on your computer
Basic knowledge of JavaScript
You don’t need any prior experience with CloudFlyer Workers.
What will you make?
Your chatbot will have two main parts:
Back end worker For centuries.src/index.js): Handles chat requests, manages sessions, and connects to AI
Front-end widget For centuries.public/widget. JS): the embeddable UI that users interact with
You will use four CloudFlyer services:
Worker AI: Powers AI responses using the MetaLlama 3 model
Vectorize: stores and searches your FAQ for relevant context (this is the rag part)
KV: Conversation history is maintained between sessions
The worker: Runs your serverless backend at the edge
How to set up a project
First, create a new CloudFlyer Workers project. Open your terminal and run the following command.
When it asks you for a programming language, select it javascriptand when it asks, “Do you want to deploy your application?” Select nosince we are going to deploy at the end.
npm create cloudflare@latest ai-chatbot-widget -- --type=hello-world
Go to your new project directory:
cd ai-chatbot-widget
And install the required development dependencies:
npm install --save-dev tailwindcss autoprefixer postcss wrangler
Your project is now ready for development.
How to Configure Wrangler
Wrangler is CloudFlyer’s command-line tool for developing and deploying workers. You need to configure it to use the required services.
a Cloud Flyer Worker is a serverless function that runs on CloudFlyer’s Global Edge network. Unlike traditional servers that run in a single location, workers operate closer to your customers using more than 300 data centers around the world. This results in faster response times and lower latency. You just write JavaScript code, and CloudFlyer takes care of all the infrastructure, scaling, and deployment.
Create resources (one-time setup)
The following resources are created using the Wrangler CLI (recommended for automation).
First, install Wrangler (if you don’t already have it):
npm install -g wrangler
To log in, use wrangler login. This command will open a CloudFlyer browser tab where you need to grant permission.
Create a vectorize index (for RAG):
a Vectorize index is a vector database that allows you to perform semantic searches. Instead of finding exact keyword matches (as in traditional databases), Vectorize finds content based on meaning.
How it works: You convert your FAQ questions and answers into numerical vectors (called embeddings) using an AI model. When a user asks a question, the chatbot converts the question into a vector and searches for FAQ entries with similar vectors. This is a “rag” (retrieval augmented generation) technique, which augments the AI’s response with relevant context from your knowledge base.
npx wrangler vectorize create faq-vectors --dimensions=768 --metric=cosine
Create a KV namespace (for session history):
KV (key-value) storage Is CloudFlyer’s globally distributed database for storing simple data? Think of it like a giant dictionary: you store data using a key (session ID) and later retrieve it using the same key.
For your chatbot, KV stores each user’s conversation history. When a user returns to your website, the chatbot retrieves their session CV and remembers the conversation they had before.
npx wrangler kv namespace create CHAT_SESSIONS
Note the ID from the output as you will add it wrangler.jsonc File
Create a file called wrangler.jsonc In the root of your project (you just need to change YOUR_KV_NAMESPACE_ID with the ID you received in the last step):
This configuration file tells Wrangler which CloudFlyer services your worker needs to access.
Let me explain the key restrictions:
Assets: Serves static files (like your widget’s JavaScript and CSS).
publicFolderAi: Connects to CloudFlyer’s worker AI to run machine learning models
Vectorize: Link to your vectorize index to store and search FAQ embeddings
chat_sessions: Connects to the KV namespace to store conversation history
How to build a backend worker
The backend worker is the brain of your chatbot. It handles incoming chat messages, searches your FAQ for relevant context, forwards the conversation to the AI, returns the response to the user, and saves everything for later.
Create the file src/index.js with this code:
const SYS = `You are a helpful customer support assistant. Be friendly, professional, and concise. Use the FAQ context to give accurate answers. If you don't know something, say so.`;
const TTL = 30*24*60*60;
const cors = '👋 Hi! How can I help you today?'
;
const json = (d, s=200, h= messages: s ? (await env.CHAT_SESSIONS.get(s, 'json'))?.messages ) => new Response(JSON.stringify(d), ''; );
const cookie = r => r.headers.get('Cookie')?.match(/chatbot_session=((^;)+)/)?.(1);
async function faq(env, q) {
try {
const e = await env.AI.run('@cf/baai/bge-base-en-v1.5', messages: s ? (await env.CHAT_SESSIONS.get(s, 'json'))?.messages );
if (!e.data) return '';
const r = await env.VECTORIZE.query(e.data(0), );
return r.matches.map(m => `Q: ${m.metadata?.question}\nA: ${m.metadata?.answer}`).join('\n\n');
} catch { return ''; }
}
async function chat(req, env) {
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
const { message } = await req.json();
if (!message?.trim()) return json({ error: 'Message required' }, 400);
let sid = cookie(req), isNew = !sid;
let sess = sid ? await env.CHAT_SESSIONS.get(sid, 'json') : null;
if (!sess) { sid = 'sess_' + crypto.randomUUID(); sess = { id: sid, messages: (), createdAt: Date.now(), updatedAt: Date.now() }; isNew = true; }
sess.messages.push({ role: 'user', content: message.trim(), timestamp: Date.now() });
const ctx = await faq(env, message);
const msgs = ({ role: 'system', content: SYS + (ctx ? `\n\nFAQ:\n${ctx}` : '') }, ...sess.messages.slice(-10).map(m => ({ role: m.role, content: m.content })));
const stream = await env.AI.run('@cf/meta/llama-3-8b-instruct', { messages: msgs, stream: true });
let full = '';
const { readable, writable } = new TransformStream({
transform(chunk, ctrl) {
for (const ln of new TextDecoder().decode(chunk).split('\n'))
if (ln.startsWith('data: ') && ln.slice(6) !== '(DONE)') try { full += JSON.parse(ln.slice(6)).response || ''; } catch {}
ctrl.enqueue(chunk);
},
async flush() {
if (full) { sess.messages.push({ role: 'assistant', content: full, timestamp: Date.now() }); sess.updatedAt = Date.now(); await env.CHAT_SESSIONS.put(sid, JSON.stringify(sess), { expirationTtl: TTL }); }
}
});
stream.pipeTo(writable);
return new Response(readable, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', ...cors, ...(isNew ? { 'Set-Cookie': `chatbot_session=${sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${TTL}` } : {}) } });
}
async function seed(req, env) {
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
const faqs = (
('How long does shipping take?', 'Standard 5-7 days, Express 2-3 days, Same-day in select areas.'),
('What is your return policy?', '30-day returns for unused items. Electronics 15 days if defective.'),
('Do you offer free shipping?', 'Yes! Orders over $50 get free standard shipping.'),
('How can I track my order?', 'Check your email for tracking or log into your account.'),
('What payment methods do you accept?', 'Visa, Mastercard, Amex, PayPal, Apple Pay, Google Pay.'),
('Do you have a warranty?', 'All products have manufacturer warranty. Extended plans available.'),
('Can I cancel my order?', 'Within 1 hour if not processed. Otherwise return after delivery.'),
('Do you ship internationally?', 'Yes, 50+ countries. 7-14 days. Duties paid by customer.'),
);
try {
const vecs = await Promise.all(faqs.map(async ((q,a), i) => {
const e = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: (q+' '+a) });
return { id: `faq-${i+1}`, values: e.data?.(0) || (), metadata: { question: q, answer: a } };
}));
await env.VECTORIZE.upsert(vecs);
return json({ success: true, count: faqs.length });
} catch { return json({ error: 'Seed failed' }, 500); }
}
export default {
async fetch(req, env) {
const p = new URL(req.url).pathname;
if (req.method === 'OPTIONS') return new Response(null, { headers: { ...cors, 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' } });
if (p === '/api/chat') return chat(req, env);
if (p === '/api/history') { const s = cookie(req); return json({ messages: s ? (await env.CHAT_SESSIONS.get(s, 'json'))?.messages || () : () }); }
if (p === '/api/seed') return seed(req, env);
if (p === '/api/health') return json({ status: 'ok' });
return env.ASSETS.fetch(req);
}
};
Let me break down the key parts of this code:
Session Management:
cookieThe function extracts the session ID from the user’s browser cookies. When a user chats for the first time, the worker generates a unique session ID, stores it in an HTTP-only cookie, and stores the conversation history in the KV. On subsequent visits, the worker retrieves the session and continues the conversation.Rag with Vectorize:
faqThe function implements RAG. It transforms the user’s query into a vector using the BGE model, then vectorizes the query queries for these three FAQ entries. This relevant context is added to the AI ​​prompt, helping the AI ​​to give accurate, grounded answers rather than making things up.Streaming response:
chatThe function uses aTransformStreamTo process the AI’s response as it moves. Each token goes to the client immediately, creating a natural typing effect. When the current is stopped, the complete reaction KV is conserved.Sowing FAQs:
seedThe function populates your FAQ database. It converts each query-response pair into a vector embedding and stores it in vectorize. After deployment you only need to call once.
Now that your backend is ready, let’s create the frontend. But first, you need to configure the Tailwind CSS to style your widget.
How to Configure Tailwind CSS
Your chatbot widget needs to look polished and professional. To achieve this, you would use Tailwind CSS Which is the first utility CSS framework that lets you style elements directly in your HTML using small, single-purpose classes. bg-blackfor , for , for , . rounded-fulland shadow-lg.
Why a tailwind? Well, traditional CSS requires you to write separate style sheets and invent class names. Tailwind eliminates this overhead by providing pre-built utility classes. This is especially useful for embeddable widgets because all styles are self-contained and won’t conflict with the host website’s CSS.
Create the file tailwind.config.js At the root of your project:
tail
module.exports = {
content: ('./public/**/*.{html,js}'),
darkMode: 'class',
theme: { extend: {} },
plugins: ()
};
This configuration tells Tailwind to scan all HTML and JavaScript files public Folder for class names. darkMode: 'class' Setting enables toggling dark mode dark Class for widget container.
Build on the source css file src/input.css:
@tailwind base;
@tailwind components;src/input.css;
@tailwind utilities;
This file imports Tailwind’s base styles, component classes, and utility classes. When you build, Tailwind will scan your code and generate a minimal CSS file that contains only the classes you actually use.
Update your package with build scripts.
{
"name": "ai-chatbot-widget",
"version": "1.0.0",
"private": true,
"scripts": {
"build:css": "npx tailwindcss -i ./src/input.css -o ./public/styles.css --minify",
"deploy": "npm run build:css && wrangler deploy",
"dev": "npm run build:css && wrangler dev"
},
"devDependencies": {
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"wrangler": "^4.56.0"
}
}
build:css The script compiles and minifies your Tailwind CSS. deploy And dev Scripts automatically generate CSS before starting or configuring the development server.
With the styles ready to go, let’s create widgets that users will actually interact with.
The frontend widget is a self-contained JavaScript file that creates the entire chat interface. When someone adds your script to their website, it automatically creates a chat bubble button, a chat window, and handles all the interactive functionality.
Create the file public/widget.js:
(function () {
'use strict';
const C = {
u: window.CHATBOT_BASE_URL || '',
t: window.CHATBOT_TITLE || 'AI Assistant',
p: window.CHATBOT_PLACEHOLDER || 'Message...',
g: window.CHATBOT_GREETING || '👋 Hi! How can I help you today?'
};
let open = 0, msgs = (), typing = 0, menu = 0;
let dark = matchMedia('(prefers-color-scheme:dark)').matches;
const $ = id => document.getElementById(id);
const tog = (e, c, on) => e.classList.toggle(c, on);
function init() {
const l = document.createElement('link');
l.rel = 'stylesheet';
l.href = C.u + '/styles.css';
document.head.appendChild(l);
const d = document.createElement('div');
d.id = 'cb';
d.innerHTML = `