Embeddable RAG FAQ-Based AI Chatbot Widget - Next.js, Redis, Vectorize FAQ Seed, Multiple AI Models, Full-Stack Project
A production-ready, self-hosted RAG (Retrieval Augmented Generation) chatbot widget built with Next.js, Upstash Redis vector storage, and a multi-provider AI fallback chain. Embed it in a portfolio site, SaaS dashboard, or any web app — with a React widget or a vanilla JS script.
- Live Demo: https://portfolio-chatbot-widget.vercel.app/
- Production Live: https://www.arnobmahmud.com/
- Security: Private reports → SECURITY.md · contact@arnobmahmud.com
- Author: Arnob Mahmud · LinkedIn: linkedin.com/in/arnob-mahmud-05839655 · GitHub: github.com/arnobt78
- What You Will Learn
- Overview
- Features
- Technology Stack
- How It Works (Architecture)
- Project Structure
- Prerequisites
- Installation & Setup
- Environment Variables
- Running the Project
- Seeding the FAQ Knowledge Base
- Usage & Embedding
- API Reference
- Frontend Architecture
- Backend & AI Pipeline
- Reusing Components in Other Projects
- Deployment
- Observability (Sentry)
- Documentation Index
- Known Limitations
- Keywords
- Conclusion
- License
- Happy Coding
By studying and running this project, you will learn:
- How to build a RAG chatbot with vector search over a FAQ knowledge base
- How to stream AI responses with Server-Sent Events (SSE) on Next.js Edge routes
- How to store sessions and embeddings in serverless Redis (Upstash)
- How to design a multi-provider AI fallback chain (Gemini → OpenRouter → Groq → Hugging Face → OpenAI)
- How to embed a chat widget via React or vanilla JavaScript
- How to manage client state with TanStack Query (cache, optimistic updates)
- How to wire Sentry with same-origin tunneling to bypass ad blockers
This repository is a full-stack Next.js application that serves two roles:
- Hosted chatbot backend — API routes for chat, history, feedback, and FAQ seeding
- Embeddable UI — floating chat widget (React in
layout.tsx, orpublic/widget.jsfor external sites)
Users ask questions in natural language. The system:
- Embeds the question as a vector
- Finds the top matching FAQs in Redis (cosine similarity)
- Injects that context into the LLM prompt
- Streams the answer back token-by-token
- Persists the conversation in Redis (30-day session cookie)
The default FAQ dataset contains 20 Q&A pairs about Arnob Mahmud (portfolio use case). Replace lib/faqs.ts with your own content for any domain.
| Feature | Description |
|---|---|
| RAG search | Semantic FAQ retrieval via embeddings + cosine similarity |
| Streaming replies | SSE from /api/chat — tokens appear as they generate |
| Session history | HttpOnly cookie chatbot_session + Redis persistence |
| AI fallbacks | Automatic provider/model chain if one API fails or rate-limits |
| Embedding fallbacks | Gemini → Hugging Face → OpenRouter → OpenAI for vectors |
| Dual embed modes | React widget (this repo) or standalone widget.js |
| Theme system | Dark/light mode, zero-flash inline script in layout.tsx |
| Mobile UX | Keyboard-aware positioning, responsive widget |
| Feature | Description |
|---|---|
| Security headers | X-Frame-Options, Referrer-Policy, etc. in next.config.ts |
| Sentry (optional) | Error tracking with /api/monitoring tunnel (ad-blocker safe) |
| CORS | Cross-origin embed support with credentials on API routes |
| Edge runtime | Fast chat/history/feedback on Vercel Edge |
| Library | Version | Role |
|---|---|---|
| Next.js | 16.1.4 | App Router, SSR layout, API routes |
| React | 19.2.3 | UI components, hooks |
| TypeScript | 5.x | Type safety |
| Tailwind CSS | 3.4 | Styling |
| TanStack Query | 5.x | Server state, chat cache key ["chat-history"] |
| Radix UI | — | Accessible dialogs, menus, toasts |
| Lucide React | — | Icons |
TanStack Query in one sentence: It fetches /api/history on load, caches messages, and applies optimistic updates when you send a message — so the UI feels instant while the stream completes.
| Library / Service | Role |
|---|---|
| Upstash Redis | Session JSON + FAQ vector hashes |
| Google Gemini | Primary chat + embeddings |
| OpenRouter | Free-tier model fallbacks (:free suffix) |
| Groq | Fast OSS model fallbacks |
| Hugging Face Router | Embedding + chat fallbacks |
| OpenAI | Optional paid last resort |
Vercel AI SDK (ai package) |
Streaming abstraction |
| @sentry/nextjs | Optional error monitoring |
| Route | Runtime |
|---|---|
/api/chat, /api/history, /api/feedback |
Edge |
/api/seed |
Node.js (longer embedding batch job) |
User message
│
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Chat Widget │────▶│ POST /api/chat │────▶│ getSession() │
│ (React / JS) │◀────│ SSE stream │◀────│ saveSession() │
└─────────────────┘ └────────┬─────────┘ └─────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
searchFAQ() getAIResponse() Redis
(RAG) (fallback chain) (Upstash)
│ │
▼ ▼
generateEmbedding Gemini / OpenRouter /
+ cosine search Groq / HF / OpenAI
RAG step (lib/rag.ts):
// 1. Embed the user question
const queryEmbedding = await generateEmbedding(query);
// 2. Compare against all FAQ vectors in Redis
const results = await searchVectors(queryEmbedding, topK);
// 3. Format top matches as LLM context
return results
.map((r) => `Q: ${r.metadata.question}\nA: ${r.metadata.answer}`)
.join("\n\n");Session cookie: New visitors get chatbot_session=sess_... (HttpOnly, SameSite=Lax, 30 days). The same ID loads history on return visits.
portfolio-chatbot-widget/
├── app/
│ ├── api/
│ │ ├── chat/route.ts # POST — SSE streaming chat
│ │ ├── history/route.ts # GET — load session messages
│ │ ├── feedback/route.ts # POST — feedback form (logs; email TBD)
│ │ └── seed/route.ts # POST — embed FAQs into Redis
│ ├── global-error.tsx # Sentry global error boundary
│ ├── layout.tsx # SSR shell + theme script + ChatbotWidget
│ ├── page.tsx # Demo landing page
│ ├── providers.tsx # TanStack Query + widget settings
│ └── robots.ts # SEO / AI crawler rules
├── components/
│ ├── chatbot/
│ │ ├── chatbot-widget.tsx # Main floating widget UI
│ │ ├── widget-menu.tsx # Settings menu (theme, font, position)
│ │ └── message-skeleton.tsx
│ └── ui/ # shadcn-style primitives (button, dialog, …)
├── contexts/
│ └── widget-settings-context.tsx # Theme, font size, position (localStorage)
├── hooks/
│ ├── use-chat.ts # TanStack Query + SSE sendMessage
│ └── use-widget-settings.ts
├── lib/
│ ├── ai/ # Provider registry + streaming orchestrator
│ │ ├── providers.ts # Model chains per provider
│ │ ├── index.ts # getAIResponse() fallback loop
│ │ ├── gemini-stream.ts
│ │ ├── openai-stream.ts
│ │ ├── normalize-messages.ts
│ │ └── retriable.ts # 429 / 5xx classification
│ ├── embeddings.ts # Multi-provider embedding generation
│ ├── faqs.ts # FAQ knowledge base (edit this!)
│ ├── rag.ts # searchFAQ()
│ ├── redis.ts # Sessions, vectors, cosine search
│ ├── sentry-env.ts # DSN + tunnel path helpers
│ ├── sentry-filters.ts # Ignore extension/browser noise
│ └── constants.ts
├── public/
│ ├── widget.js # Vanilla embed script for external sites
│ └── styles.css # Widget styles (shared)
├── docs/ # Deployment, guardrails, integration guides
├── instrumentation.ts # Sentry server/edge bootstrap
├── instrumentation-client.ts # Sentry client + tunnel
├── sentry.server.config.ts
├── sentry.edge.config.ts
├── next.config.ts # Security headers + Sentry wrapper
├── .env.example # All env vars (copy to .env.local)
└── SECURITY.md # Private vulnerability reporting
- Node.js 24.x (see
.nvmrcandpackage.jsonengines) - npm (or pnpm/yarn)
- Upstash Redis account (free tier works)
- Google Gemini API key (primary AI)
- Hugging Face token (embedding fallback — recommended)
- Optional: OpenRouter, Groq, OpenAI keys for deeper fallback coverage
- Optional: Sentry project for error monitoring
git clone https://github.com/arnobt78/portfolio-chatbot-widget.git
cd portfolio-chatbot-widget
npm installcp .env.example .env.localEdit .env.local — see Environment Variables below.
npm run dev
# In another terminal (set SEED_SECRET in .env.local first):
curl -X POST http://localhost:3000/api/seed \
-H "Authorization: Bearer $SEED_SECRET"Expected response: {"success":true,"count":20}
Visit http://localhost:3000 and use the widget in the bottom-right corner.
Copy from .env.example. Never commit .env.local.
| Variable | Description | Where to get |
|---|---|---|
UPSTASH_REDIS_URL |
Upstash REST URL | console.upstash.com → Database → REST API |
UPSTASH_REDIS_TOKEN |
Upstash REST token | Same as above |
GOOGLE_GEMINI_API_KEY |
Primary LLM + embeddings | aistudio.google.com/apikey |
HUGGING_FACE_API_KEY |
Embedding fallback | huggingface.co/settings/tokens |
| Variable | Description |
|---|---|
OPENROUTER_API_KEY |
Free-tier models via OpenRouter (:free chain) |
GROQ_API_KEY |
Groq OSS models |
OPENAI_API_KEY |
Paid last-resort chat + embeddings |
Alternate names supported: OpenRouter_API_KEY, Groq_Llama_API_KEY, Hugging_Face_Inference_API_KEY.
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_CHATBOT_URL |
http://localhost:3000 |
Public deploy URL (embed + API base) |
NEXT_PUBLIC_SITE_URL |
— | OpenRouter HTTP-Referer fallback |
CHATBOT_TITLE |
Chat Assistant |
Widget header title |
CHATBOT_GREETING |
👋 How can I help… |
First message bubble |
CHATBOT_PLACEHOLDER |
Message... |
Input placeholder |
SESSION_TTL |
2592000 (30 days) |
Redis session TTL in seconds |
SEED_SECRET |
— | Required — protects POST /api/seed |
FEEDBACK_EMAIL |
— | Intended recipient (email sending not wired yet) |
| Variable | Description |
|---|---|
NEXT_PUBLIC_SENTRY_DSN |
Browser DSN (required for client errors) |
SENTRY_DSN |
Server alias (falls back to public DSN) |
SENTRY_ORG |
Org slug — build-time source maps |
SENTRY_PROJECT |
Project slug (not org name) |
SENTRY_AUTH_TOKEN |
CI/Vercel auth token for source map upload |
When DSN is empty, Sentry is disabled — no runtime overhead.
UPSTASH_REDIS_URL=https://xxxx.upstash.io
UPSTASH_REDIS_TOKEN=AXxxxx
GOOGLE_GEMINI_API_KEY=AIza...
HUGGING_FACE_API_KEY=hf_...
NEXT_PUBLIC_CHATBOT_URL=http://localhost:3000| Command | Purpose |
|---|---|
npm run dev |
Development server at http://localhost:3000 |
npm run build |
Production build (TypeScript + Next.js) |
npm start |
Run production server locally |
npm run lint |
ESLint |
Node version: Use Node 24 (nvm use reads .nvmrc).
The seed endpoint reads lib/faqs.ts, generates embeddings in batches, and stores vectors in Redis.
curl -X POST https://your-domain.com/api/seed \
-H "Authorization: Bearer $SEED_SECRET"SEED_SECRET must be set in .env.local / Vercel — requests without a valid secret return 401; if unset server-side, returns 503.
When to re-seed: After editing lib/faqs.ts or changing embedding models.
Customize FAQs: Edit the faqs array in lib/faqs.ts — each entry is [question, answer].
With Bot Protection = Challenge enabled (recommended), raw curl from a cold terminal may return 429. Reseed is not blocked — use one of these:
Option A — Browser DevTools (easiest, no firewall changes)
- Open https://portfolio-chatbot-widget.vercel.app in Safari/Chrome.
- DevTools → Console:
fetch('/api/seed', {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_SEED_SECRET' }
}).then(r => r.json()).then(console.log)
// Expected: { success: true, count: 20 }Option B — Terminal curl (same machine)
- Visit the site in a browser first (passes the Vercel challenge cookie).
- Then run:
curl -X POST https://portfolio-chatbot-widget.vercel.app/api/seed \
-H "Authorization: Bearer $SEED_SECRET"Option C — Automated cron/CI only (optional)
Add a Vercel Firewall exception for POST /api/seed if you need hands-off reseed without a browser visit. Not required for normal use.
Already mounted in app/layout.tsx via <ChatbotWidget />. Config comes from env vars injected into window:
window.CHATBOT_BASE_URL = "https://your-app.vercel.app";
window.CHATBOT_TITLE = "Chat Assistant";
window.CHATBOT_GREETING = "👋 How can I help you today?";
window.CHATBOT_PLACEHOLDER = "Message...";On your external site (e.g. WordPress, static HTML):
<script>
window.CHATBOT_BASE_URL = "https://portfolio-chatbot-widget.vercel.app";
window.CHATBOT_TITLE = "Portfolio Assistant";
</script>
<script
src="https://portfolio-chatbot-widget.vercel.app/widget.js"
async
></script>The script creates a floating button, loads /styles.css from your deployment, and calls the same /api/chat and /api/history endpoints with cookies.
- Copy
components/chatbot/,hooks/use-chat.ts,contexts/widget-settings-context.tsx - Copy
public/styles.css - Point
CHATBOT_BASE_URLat your deployed API origin - Ensure CORS + credentials work for your domain
Stream a chat response.
Request:
{ "message": "Tell me about Arnob Mahmud" }Headers: Cookie: chatbot_session=... (optional — created if missing)
Response: text/event-stream
data: {"response":"Hello"}
data: {"response":" there"}
data: [DONE]
Runtime: Edge
Return messages for the current session cookie.
Response:
{
"messages": [
{ "role": "user", "content": "Hi", "timestamp": 1700000000000 },
{ "role": "assistant", "content": "Hello!", "timestamp": 1700000001000 }
]
}Runtime: Edge
Clear the current session on the server and expire the session cookie. Used by Clear Chat / New Chat in the widget.
Response: { "success": true }
Runtime: Edge
Embed all FAQs from lib/faqs.ts into Redis. Requires auth:
Authorization: Bearer <SEED_SECRET>
# or
x-seed-secret: <SEED_SECRET>Response: { "success": true, "count": 20 }
Runtime: Node.js
Submit widget feedback or issue report.
Request:
{
"type": "feedback",
"rating": 5,
"comment": "Great widget!",
"email": "user@example.com"
}Note: Logs to server console; email integration is TODO.
Runtime: Edge
All chat/history/feedback routes support CORS with Access-Control-Allow-Credentials: true for cross-origin embeds.
- Query key:
["chat-history"]— loads history on mount - Optimistic update: User message appears immediately on send
- Streaming: Parses SSE chunks and updates assistant message in cache
- Rollback: On error, restores previous cache snapshot
const { messages, sendMessage, isLoading, clearChat } = useChat();Persists to localStorage:
- Theme (light / dark)
- Font size
- Widget position (left / right)
An inline <script> in layout.tsx runs before paint, reads localStorage, and sets CSS variables — preventing a white flash on dark mode.
Chat providers are tried in order. Within each provider, models are tried until one succeeds. On HTTP 429, remaining models for that provider are skipped (fast-skip).
| Order | Provider | Example models |
|---|---|---|
| 1 | Gemini | gemini-2.5-flash, gemini-2.5-flash-lite |
| 2 | OpenRouter | openai/gpt-oss-20b:free, … |
| 3 | Groq | openai/gpt-oss-120b, … |
| 4 | Hugging Face | openai/gpt-oss-20b, … |
| 5 | OpenAI | gpt-4o-mini (paid) |
See docs/LLM_MODEL_SELECTION.md for verification notes and update policy.
Used by RAG search and /api/seed. Fallback chain: Gemini → Hugging Face Router → OpenRouter free embedding → OpenAI.
| Key pattern | Content |
|---|---|
chat:session:{id} |
JSON session with messages (TTL) |
chat:vectors:faq-{n} |
Hash: vector, metadata (question + answer) |
Vector search uses in-process cosine similarity over all FAQ keys (fine for ~20–100 FAQs; for larger scale, use RediSearch or a dedicated vector DB).
| Piece | Reuse strategy |
|---|---|
ChatbotWidget |
Drop into any Next.js layout.tsx with Providers |
useChat |
Change CHATBOT_BASE_URL or pass custom API base |
lib/faqs.ts |
Replace Q&A content — your domain knowledge |
lib/ai/providers.ts |
Adjust model IDs when providers deprecate models |
public/widget.js |
Zero-build embed for non-React sites |
| Integration guide | docs/Redis_Sentry_PostHog_INTEGRATION_GUIDE.md — portable Redis/Sentry/PostHog patterns |
Minimal external embed checklist:
- Deploy this app (or fork) with env vars set
POST /api/seedonce (withAuthorization: Bearer $SEED_SECRET)- Add
widget.js+CHATBOT_BASE_URLto your site - Ensure your domain is allowed by CORS (currently reflects request
Origin)
- Push to GitHub
- Import repo in vercel.com
- Add all env vars from
.env.example - Deploy — Node 24 picked up via
enginesfield - Post-deploy: re-seed using Production re-seed (browser DevTools or curl after visiting site)
- Set
NEXT_PUBLIC_CHATBOT_URLto your Vercel URL
See docs/DEPLOYMENT.md for full steps including Vercel Firewall (Bot Challenge + AI Bots Deny).
npm run build
npm startRequires Node 24 and the same env vars.
Optional error tracking with ad-blocker-safe tunnel:
- Client events POST to same-origin
/api/monitoring - Server forwards to Sentry ingest
- Noise filters drop browser extension errors
Setup details: docs/Redis_Sentry_PostHog_INTEGRATION_GUIDE.md
| Document | Purpose |
|---|---|
| docs/PROJECT_WALKTHROUGH.md | Agent/dev quick reference |
| docs/DEPLOYMENT.md | Vercel/VPS deploy steps |
| docs/LLM_MODEL_SELECTION.md | Free-tier model research |
| docs/VERCEL_PRODUCTION_GUARDRAILS.md | Headers, robots, firewall |
| docs/Redis_Sentry_PostHog_INTEGRATION_GUIDE.md | Portable integration patterns |
| docs/AGILE_V_PROTOCOL.md | Project planning protocol |
| SECURITY.md | Private vulnerability reporting |
| Item | Status |
|---|---|
| API rate limiting | Not implemented at HTTP layer (AI layer has 429 fast-skip) |
| Feedback email | Logs only — no Resend/SendGrid yet |
| PostHog analytics | Documented as optional template, not wired in code |
| Large FAQ corpora | In-memory cosine scan — migrate to vector DB at scale |
RAG · Retrieval Augmented Generation · Next.js App Router · Edge Runtime · Server-Sent Events · SSE streaming · Upstash Redis · vector search · cosine similarity · embeddings · Google Gemini · OpenRouter · Groq · Hugging Face · chatbot widget · embeddable widget · portfolio chatbot · TanStack Query · React 19 · TypeScript · Tailwind CSS · self-hosted AI · FAQ bot · Sentry tunnel · open source
This project demonstrates a complete, deployable RAG chatbot you can host yourself, customize with your own FAQs, and embed anywhere. The architecture prioritizes reliability (multi-provider fallbacks), performance (Edge streaming), and developer experience (typed hooks, clear API routes, portable docs).
Fork it, swap lib/faqs.ts, adjust branding env vars, and you have a production-grade assistant for your portfolio or product docs.
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project - feel free to use, enhance, and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.arnobmahmud.com.
Security: Please report vulnerabilities privately via SECURITY.md → contact@arnobmahmud.com.






