{"slug": "building-a-production-ai-chatbot-for-an-educational-institute-architecture-full", "title": "Building a Production AI Chatbot for an Educational Institute: Architecture, Lessons & Full Stack Deep-Dive", "summary": "Here is a factual summary of the article:\n\nThe article describes the architecture and development of a full-stack AI chatbot platform built for IFDA, an Indian educational institute, using a Next.js 14 monorepo deployed on Vercel with a Neon PostgreSQL backend. The chatbot handles course discovery, lead capture, appointment scheduling, and WhatsApp messaging, employing a two-tier approach that uses scripted responses for routine queries and GPT only when necessary to reduce costs and latency. The system features a block-based response architecture that allows the same engine output to be rendered for both the web UI and WhatsApp through separate channel-specific renderers.", "body_md": "TL;DR: I built a full-stack AI chatbot platform for IFDA, an Indian educational institute. It handles course discovery, lead capture, appointment scheduling, WhatsApp messaging, and comes with a complete admin CRM — all deployed on Vercel with a Neon PostgreSQL backend. Here's everything I learned, every system I built, and why I made the choices I did.\nEducational institutes spend enormous resources on human admissions counselors. Prospects ask the same questions repeatedly: What courses do you offer? How long is the program? What will I earn afterwards? And every unanswered query at 11 PM is a lost lead.\nIFDA needed something smarter than a static FAQ page and cheaper than a 24/7 call center. The solution: a hybrid AI chatbot that could hold intelligent conversations about courses, capture leads, book counseling appointments, and hand off hot prospects to human staff — all while syncing with WhatsApp.\nThis is the full technical story of how I built it.\nThe project is a Next.js 14 App Router monorepo — one codebase serving the public-facing chatbot widget, the admin CRM dashboard, and all API routes. Every component was chosen deliberately:\nThe graph of this codebase has 1,259 nodes (files, functions, types) and 3,276 edges (calls, contains, references) distributed across 55+ community clusters — from the bot engine core, to the Prisma edge runtime, to the admin dashboard pages. Let's walk through each major system.\nlib/bot/engine.ts\n)\nThe heart of the project is processMessage()\nin lib/bot/engine.ts\n. This single function is the traffic controller for every incoming message — whether it arrives from the web widget or WhatsApp.\nThe bot does not send every message to GPT. That would be slow and expensive. Instead, it uses a two-tier approach:\nThe stage management lives in lib/ai/intent.ts\n:\n// intent.ts — simplified\nexport function resolveStage(session: Session): FunnelStage { ... }\nexport function getMissingFields(lead: Partial<Lead>): LeadField[] { ... }\nexport function getNextQuestion(missing: LeadField[]): string { ... }\nexport function isLeadComplete(lead: Partial<Lead>): boolean { ... }\nexport function detectIntent(message: string): Intent { ... }\nexport function analyzeMessage(message: string, session: Session): Analysis { ... }\nanalyzeMessage()\nis the decision function. It evaluates the message in context of the current session stage and decides whether to invoke the scripted path, call getAIResponse()\nfrom lib/ai/llm.ts\n, or trigger a carousel/quick-reply block.\nResponses are not plain strings. They're typed message blocks that the renderer layer converts to channel-specific formats:\n// lib/ai/llm.ts — block builders\ntextBlock(content: string): TextBlock\ndomainListBlock(domains: Domain[]): DomainListBlock\ncarouselBlock(items: CourseCarouselItem[]): CarouselBlock\ncourseDetailBlock(course: Course): CourseDetailBlock\nquickRepliesBlock(replies: string[]): QuickRepliesBlock\nvisitWebsiteBlock(url: string): VisitWebsiteBlock\nThis block-based design means the same engine output drives both the web UI and WhatsApp — two very different rendering surfaces.\nThe renderer layer translates structured message blocks into channel-specific formats.\nlib/bot/renderers/web.ts\n)\nrenderToWeb()\nconverts blocks into React-compatible JSON that the frontend StructureMessage.tsx\ncomponent consumes. The component handles scroll behavior, carousels with checkScroll()\n, and animated message appearance.\nlib/bot/renderers/whatsapp.ts\n)\nrenderToWhatsApp()\nmaps the same blocks to the DoubleTick API's message format. WhatsApp has strict message type rules — interactive lists, templates, and plain text are separate API calls with different schemas. The renderer handles all of this through the lib/whatsapp/doubletick.ts\nclient:\n// doubletick.ts\nexport async function sendWhatsAppText(phone: string, text: string): Promise<void>\nexport async function sendWhatsAppTemplate(phone: string, template: TemplateParams): Promise<void>\nexport async function sendWhatsAppInteractiveList(phone: string, list: InteractiveList): Promise<void>\nIncoming WhatsApp messages hit app/api/whatsapp/webhook/route.ts\n, which validates the HMAC signature via isValidSignature()\n, parses the payload through lib/whatsapp/messageParser.ts\n, and feeds it into the same processMessage()\nbot engine. One engine, two channels.\nlib/ai/courseData.ts\n)\nThe course catalog is the richest data source in the system. courseData.ts\nis a comprehensive module with 25+ exported functions that the bot engine calls to build contextually accurate responses:\nfindCourse(query: string): Course | null\ngetCourseAbout(course: Course): string\ngetCoursePrerequisites(course: Course): string\ngetCourseSyllabus(course: Course): string\ngetCourseCareerOpportunities(course: Course): string\ngetCourseCareerGrowthRoadmap(course: Course): string\ngetCourseProfessionalGrowthLadder(course: Course): string\ngetCourseTools(course: Course): string\ngetCourseSkills(course: Course): string\ngetAllCourseCarouselItems(): CourseCarouselItem[]\ngetCarouselByIntent(intent: Intent): CourseCarouselItem[]\ngetLLMCourseMap(): LLMCourseMap // Compact map for GPT context injection\ngetCourseContext(course: Course): string // Full context string for LLM prompt\nfindCourse()\nis the most-called function in the entire application (32 edges in the call graph), supporting fuzzy matching so \"digital marketing,\" \"DM course,\" and \"marketing program\" all resolve correctly.\ngetLLMCourseMap()\nis particularly clever: it generates a compact, token-efficient representation of the course catalog that gets injected into GPT prompts. This gives the LLM accurate knowledge of IFDA's offerings without burning context on verbose descriptions.\nlib/ai/knowledge-parser.ts\n+ Embeddings)\nBeyond the structured course catalog, IFDA staff can upload arbitrary knowledge documents — PDFs, CSVs, text files, JSON — through the admin panel. The knowledge pipeline processes these into searchable vector embeddings.\nknowledge-parser.ts\nhandles multi-format ingestion:\nparseKnowledgeFile(file: File): Promise<ParsedKnowledge>\nparsePdf(buffer: Buffer): Promise<string>\nparseCsv(content: string): ParsedRow[]\nparseJson(content: string): ParsedKnowledge\nparseTxt(content: string): ParsedKnowledge\nchunkText(text: string, chunkSize: number): string[]\nlib/ai/embeddings.ts\nhandles the vector layer:\ngenerateEmbedding(text: string): Promise<number[]> // OpenAI text-embedding-3-small\nembedAllFaqs(): Promise<void> // Bulk embed on import\nembedNewFaqs(): Promise<void> // Incremental update\nsearchSimilarFaqs(query: string, topK: number): Promise<FAQ[]>\nWhen a user asks a question not covered by the scripted engine, getRelevantKnowledge()\nfires a vector similarity search against the embedded knowledge base. The top results are injected into the LLM context, giving the bot accurate, up-to-date answers based on admin-uploaded documents — no redeployment required.\nLead capture is deeply woven into the conversation flow. As the bot collects information across multiple turns, it progressively builds a lead profile. extractLeadInfo()\nin intent.ts\nextracts structured data from natural language:\n// User says: \"I'm Rohan from Lucknow, interested in the UI/UX course\"\n// extractLeadInfo() returns:\n{ name: \"Rohan\", city: \"Lucknow\", courseInterest: \"UI/UX Design\" }\nverifyHumanName()\nguards against bot-abuse by rejecting implausible name strings before they reach the database.\nOnce isLeadComplete()\nreturns true, pushLeadToCRM()\nfires:\n// lib/crm/leads.ts\nexport async function pushLeadToCRM(lead: CompleteLead): Promise<void>\nThis writes to the Neon PostgreSQL database via Prisma and optionally triggers a WhatsApp confirmation template to the lead's number.\nThe admin side exposes full CRUD via:\nGET/POST /api/admin/leads\n— list and filter leadsGET/PATCH/DELETE /api/admin/leads/[id]\n— individual lead managementThe AdminLeadsPage\nin app/admin/page.tsx\nprovides a CRM interface with stage tracking — counselors can move leads through the admissions funnel manually, and updateLeadStage()\npersists the change.\nlib/scheduler/calendar.ts\nhandles the booking flow:\ngetAvailableDates(): Promise<AvailableDate[]>\nisSlotAvailable(date: string, time: string): Promise<boolean>\nbookAppointment(lead: Lead, slot: TimeSlot): Promise<Appointment>\nWhen intent detection identifies scheduling intent (handleScheduling()\nin the chatbot route), the bot presents available slots as a quick-reply carousel. The user picks one, and bookAppointment()\nwrites the appointment to the database and sends a WhatsApp confirmation.\napp/api/schedule/route.ts\nis the public endpoint, and app/api/admin/appointments/route.ts\ngives admins a view of all upcoming appointments.\nThe bot is stateful across multiple HTTP requests. lib/session/store.ts\nmanages this:\ngetSession(sessionId: string): Promise<Session | null>\nsaveSession(sessionId: string, session: Session): Promise<void>\nclearSession(sessionId: string): Promise<void>\nOn the client side, app/chatbot/hooks/useChat.ts\nmanages the sessionId\nlifecycle:\nexport function useChat() {\n// Generates and persists a sessionId\n// Manages message history\n// Handles loading/error states\nconst { sessionId } = getSessionId()\n// ...\n}\nThe session carries funnel stage, partial lead data, conversation history, and the last detected intent — everything processMessage()\nneeds to pick up exactly where the conversation left off.\nThe admin panel is a full internal application with multiple pages:\nThe permission system uses a role-permission matrix architecture, documented in implementation_plan.md\nand implemented in lib/auth/permissions.ts\n:\nexport function hasPermission(role: AdminRole, permission: Permission): boolean\nexport function getDefaultPage(role: AdminRole): string\nThe middleware chain enforces this at the API level:\n// lib/auth/middleware.ts\nrequireAdmin() // Verifies JWT, rejects unauthenticated requests\nrequireRole(role: AdminRole) // Enforces role-based access\ngetAdminFromRequest() // Extracts admin context from token\nlib/auth/jwt.ts\nhandles token lifecycle:\nsignToken(payload: JWTPayload): string\nverifyToken(token: string): JWTPayload | null\nverifyTokenAsync(token: string): Promise<JWTPayload>\nThe sidebar in the admin UI uses hasPermission()\nto filter navigation items — counselors see leads and conversations; superadmins see everything including member management and analytics.\nThe /admin/templates\npage deserves special mention. It's an AI-powered editor where admins compose multi-slide WhatsApp broadcast campaigns:\ngenerateWithAI() // GPT generates slide content from a prompt\naddSlide() / removeSlide() // Manage campaign structure\nbuildWhatsAppText() // Compile slides to WhatsApp text format\nbuildHTMLBlock() // Compile to HTML preview\nAdmins write a brief like \"announce the new UI/UX batch starting June 15,\" and the AI generates formatted, WhatsApp-compliant message slides they can edit before sending.\napp/api/admin/analytics/route.ts\naggregates metrics from the database — lead conversion rates, conversation volumes, popular courses, appointment completion rates. The dashboard BarChart\ncomponent visualizes these in real time for the admin team.\ntypes/analytics.ts\ndefines the typed response schema, and prisma/scripts/testAnalytics.ts\nwas used during development to seed realistic test data and validate aggregation queries.\nAll data flows through Prisma ORM on Neon PostgreSQL. Key models include:\nPrisma generates a full Edge-compatible client (generated/prisma/\n) for Vercel's Edge Runtime, enabling low-latency database queries from serverless functions.\nProblem: Multi-turn conversations were getting corrupted when partial lead data was included in the messages array alongside system context. GPT would \"remember\" previous context injections as user messages.\nSolution: Separated session state from conversation history. The messages\narray sent to GPT contains only actual user/assistant turns. Lead context and session state are injected exclusively in the system prompt, rebuilt fresh", "url": "https://wpnews.pro/news/building-a-production-ai-chatbot-for-an-educational-institute-architecture-full", "canonical_source": "https://dev.to/nitin7414/building-a-production-ai-chatbot-for-an-educational-institute-architecture-lessons-full-stack-32kn", "published_at": "2026-05-23 10:07:25+00:00", "updated_at": "2026-05-23 10:33:39.367658+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "large-language-models", "developer-tools", "enterprise-software"], "entities": ["IFDA", "Next.js", "Vercel", "Neon PostgreSQL", "Prisma", "WhatsApp"], "alternates": {"html": "https://wpnews.pro/news/building-a-production-ai-chatbot-for-an-educational-institute-architecture-full", "markdown": "https://wpnews.pro/news/building-a-production-ai-chatbot-for-an-educational-institute-architecture-full.md", "text": "https://wpnews.pro/news/building-a-production-ai-chatbot-for-an-educational-institute-architecture-full.txt", "jsonld": "https://wpnews.pro/news/building-a-production-ai-chatbot-for-an-educational-institute-architecture-full.jsonld"}}