<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nitin7414</title>
    <description>The latest articles on DEV Community by nitin7414 (@nitin7414).</description>
    <link>https://dev.to/nitin7414</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F744637%2Ffbe78bee-d517-4be4-9bf0-211fc1f5561d.jpeg</url>
      <title>DEV Community: nitin7414</title>
      <link>https://dev.to/nitin7414</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nitin7414"/>
    <language>en</language>
    <item>
      <title>I Mapped My Entire Next.js Task Manager as a Code Graph — Here's What It Revealed</title>
      <dc:creator>nitin7414</dc:creator>
      <pubDate>Sun, 07 Jun 2026 05:10:39 +0000</pubDate>
      <link>https://dev.to/nitin7414/i-mapped-my-entire-nextjs-task-manager-as-a-code-graph-heres-what-it-revealed-4cgl</link>
      <guid>https://dev.to/nitin7414/i-mapped-my-entire-nextjs-task-manager-as-a-code-graph-heres-what-it-revealed-4cgl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67yrhzq84wy3ua39mjtp.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67yrhzq84wy3ua39mjtp.PNG" alt=" "&gt;&lt;/a&gt;I've been building &lt;strong&gt;TaskFlow&lt;/strong&gt; — an internal task management platform for IDFA Digital AI Institute — for several months now. JWT auth, RBAC, employee management, performance analytics, WhatsApp/email reminders, file uploads, calendar events. It's grown.&lt;/p&gt;

&lt;p&gt;At some point you stop "knowing" your codebase and start &lt;em&gt;navigating&lt;/em&gt; it. So I ran a static code graph analysis over the entire project: 80 files, ~57,000 words of code, extracted into &lt;strong&gt;215 nodes and 199 edges&lt;/strong&gt;, clustered into 54 communities.&lt;/p&gt;

&lt;p&gt;What came back was a blueprint I didn't entirely expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack at a Glance
&lt;/h2&gt;

&lt;p&gt;Before we dig into the graph, here's what we're working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: Next.js 15 (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORM&lt;/strong&gt;: Prisma 7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL via Neon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State&lt;/strong&gt;: Zustand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling&lt;/strong&gt;: Tailwind v4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: Custom JWT (&lt;code&gt;signToken&lt;/code&gt; / &lt;code&gt;verifyToken&lt;/code&gt;) + middleware proxy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Uploads&lt;/strong&gt;: UploadThing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt;: Email (&lt;code&gt;nodemailer&lt;/code&gt;) + WhatsApp API
The API surface is entirely Next.js Route Handlers — no Express, no separate backend. Everything lives inside &lt;code&gt;src/app/api/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is a Code Graph, Anyway?
&lt;/h2&gt;

&lt;p&gt;A code graph treats every function, file, and component as a &lt;strong&gt;node&lt;/strong&gt;, and every &lt;code&gt;calls&lt;/code&gt;, &lt;code&gt;imports&lt;/code&gt;, or &lt;code&gt;contains&lt;/code&gt; relationship as an &lt;strong&gt;edge&lt;/strong&gt;. Run community-detection over that graph and you get natural clusters — modules that are more connected to each other than to the rest of the codebase.&lt;/p&gt;

&lt;p&gt;The tool I used extracted &lt;strong&gt;92% of edges directly from static analysis&lt;/strong&gt; and inferred the remaining 8% from structural context (with an average confidence score of 0.8). That's the "surprising connections" section — more on that below.&lt;/p&gt;




&lt;h2&gt;
  
  
  The God Nodes
&lt;/h2&gt;

&lt;p&gt;The most-connected nodes in a codebase are your &lt;strong&gt;de facto architectural pillars&lt;/strong&gt;. In graph theory, these are high-degree nodes — the traffic hubs everything routes through.&lt;/p&gt;

&lt;p&gt;Here are mine, ranked by edge count:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Edges&lt;/th&gt;
&lt;th&gt;What It Means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Every read in the app flows through here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Auth, creation, integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PATCH()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Updates for tasks, users, templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getSession()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Session validation is called &lt;em&gt;everywhere&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Soft and hard deletes across resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxy()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Middleware gatekeeper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;formatDateRange()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Date logic reused across 4+ pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;buildActivitySentence()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Activity feed string builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;formatDateShort()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;startTimer()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Timer inside &lt;code&gt;AssignTaskModal&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What This Actually Tells You
&lt;/h3&gt;

&lt;p&gt;The top 5 are all HTTP verbs or session logic — that's expected. But &lt;code&gt;formatDateRange()&lt;/code&gt; and &lt;code&gt;buildActivitySentence()&lt;/code&gt; sitting at #7 and #8 with 4 edges each is a flag.&lt;/p&gt;

&lt;p&gt;These are &lt;strong&gt;pure utility functions that live inside a page file&lt;/strong&gt; (&lt;code&gt;holidays/page.tsx&lt;/code&gt;) rather than in &lt;code&gt;src/lib/&lt;/code&gt;. They're being referenced from 4 different contexts, which means they've quietly become shared infrastructure — but they're not &lt;em&gt;treated&lt;/em&gt; as shared infrastructure. They should be extracted into a &lt;code&gt;src/lib/date.ts&lt;/code&gt; or &lt;code&gt;src/lib/activity.ts&lt;/code&gt; module before they become a maintenance burden.&lt;/p&gt;

&lt;p&gt;The graph just surfaced something I would have noticed eventually — but later.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture in Communities
&lt;/h2&gt;

&lt;p&gt;The community detection grouped the code into some natural-feeling clusters and some surprising ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community 0 — The Core Hub (cohesion: 0.1)
&lt;/h3&gt;

&lt;p&gt;The lowest cohesion score, with 8 nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sendEmailTaskReminder(), MyTasksContent(), ProfilePage(),
emptyHML(), GET(), POST(), getSession(), sendWhatsAppTaskReminder()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Low cohesion in Community 0 isn't bad — it &lt;em&gt;is&lt;/em&gt; the core. It's the hub that connects auth, user API, notification dispatch, and primary page content. A sprawling community with low internal cohesion means it bridges many others. The &lt;code&gt;GET()&lt;/code&gt; and &lt;code&gt;POST()&lt;/code&gt; handlers living here confirm this: they're cross-community bridges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community 1 &amp;amp; 4 — The Timer/Recording Module (cohesion: 0.11 and 0.22)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pauseRecording(), resumeRecording(), startRecording(),
startTimer(), stopRecording(), stopTimer()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These appear &lt;em&gt;twice&lt;/em&gt; in the graph (Communities 1 and 4) — a duplicate community that happens because the same functions exist in two contexts: once inside &lt;code&gt;AssignTaskModal.tsx&lt;/code&gt; and once in a separate recording hook. This is a classic sign of copy-paste reuse that should be unified into a single &lt;code&gt;useRecorder&lt;/code&gt; hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community 6 — Date Utility Cluster (cohesion: 0.43)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;buildActivitySentence(), formatDate(), formatDateRange(),
formatDateShort(), formatTime()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;code&gt;src/lib/date.ts&lt;/code&gt; file that doesn't exist yet. The graph carved it out naturally — these functions belong together and are already acting as a unit. The refactor is obvious once you see it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community 7 — Auth &amp;amp; Access Control (cohesion: 0.29)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signToken(), verifyToken(), proxy(), canAccess()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the security layer. &lt;code&gt;proxy()&lt;/code&gt; in &lt;code&gt;src/proxy.ts&lt;/code&gt; acts as the middleware that sits in front of all authenticated routes, calling &lt;code&gt;verifyToken()&lt;/code&gt; and then &lt;code&gt;canAccess()&lt;/code&gt; from &lt;code&gt;rbac.ts&lt;/code&gt; before requests reach any handler. Clean separation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community 15 — Performance Analytics (cohesion: 0.67)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ScoreBar(), scorePct()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;High cohesion, small scope. &lt;code&gt;PerformanceAnalytics.tsx&lt;/code&gt; is a self-contained component with its own internal utility (&lt;code&gt;scorePct()&lt;/code&gt;) and a tight render cycle. The graph essentially validated that this component is well-encapsulated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Surprising Connections
&lt;/h2&gt;

&lt;p&gt;The 8% inferred edges were the most interesting part of the report. The static analyzer couldn't directly trace these links but reasoned them from co-location and call patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AllTasksContent() → GET()
  my-tasks/page.tsx → api/users/route.ts

MyTasksContent() → GET()
  all-tasks/page.tsx → api/users/route.ts

GET() → fetchNotifications()
  api/users/route.ts → components/layout/TopNav.tsx

POST() → signToken()
  api/users/route.ts → lib/auth.ts

proxy() → GET()
  src/proxy.ts → api/users/route.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two (&lt;code&gt;AllTasksContent&lt;/code&gt; and &lt;code&gt;MyTasksContent&lt;/code&gt; both calling &lt;code&gt;GET()&lt;/code&gt; at the users endpoint) make complete sense — both pages fetch the user list for the "Assign To" dropdown but do it via &lt;code&gt;fetch('/api/users')&lt;/code&gt; inside a &lt;code&gt;useEffect&lt;/code&gt;. The graph correctly inferred this even though the static link is a URL string, not a direct import.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;GET() → fetchNotifications()&lt;/code&gt; inferred edge is more interesting. The users endpoint and the notification fetch aren't obviously linked in the source, but they share a session check and are called in sequence during the initial page load. The analyzer spotted the pattern.&lt;/p&gt;

&lt;p&gt;The one I want to verify and potentially refactor: &lt;code&gt;proxy() → GET()&lt;/code&gt;. The middleware routes through to &lt;code&gt;api/users/route.ts&lt;/code&gt; for permission validation. That's a tight coupling between the auth proxy and the user data API — worth reviewing whether the RBAC check should be reading from the session token alone rather than re-fetching user data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Knowledge Gaps the Graph Flagged
&lt;/h2&gt;

&lt;p&gt;54 communities for 215 nodes means a lot of &lt;strong&gt;singleton and doubleton communities&lt;/strong&gt; (Communities 24–53 mostly). These are "thin communities" — isolated files or pairs that haven't been wired in deeply enough.&lt;/p&gt;

&lt;p&gt;Some worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;useTimeTheme.ts&lt;/code&gt; (Community 36)&lt;/strong&gt; — my custom time-based theming hook. It shows up as an isolated doubleton, meaning it's only referenced in one place. Given that the whole calendar UI was built around it, I'd expect it to be imported in more components. This is a gap: either the hook's usage is too narrow, or other components are reimplementing the same logic locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;useTaskStore.ts&lt;/code&gt; / &lt;code&gt;filterTasks()&lt;/code&gt; (Community 39)&lt;/strong&gt; — the Zustand store for task state. Also isolated. In theory, this should be imported everywhere task state is consumed. Finding it as a thin community means some pages are probably fetching tasks directly from the API on mount instead of reading from the store — inconsistent state management that could cause stale UI bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;uploadthing-client.ts&lt;/code&gt; and &lt;code&gt;uploadthing.ts&lt;/code&gt; (Communities 50–51)&lt;/strong&gt; — both isolated singletons. UploadThing is wired in but not deeply connected to the broader graph, which is actually fine — it's an integration boundary and should be isolated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architectural Takeaways
&lt;/h2&gt;

&lt;p&gt;Running this analysis gave me a concrete refactor list:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Extract the date utilities&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;formatDate&lt;/code&gt;, &lt;code&gt;formatDateShort&lt;/code&gt;, &lt;code&gt;formatDateRange&lt;/code&gt;, &lt;code&gt;formatTime&lt;/code&gt;, and &lt;code&gt;buildActivitySentence&lt;/code&gt; are already a community. Move them to &lt;code&gt;src/lib/date.ts&lt;/code&gt; and &lt;code&gt;src/lib/activity.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Consolidate the timer/recording logic&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;startTimer&lt;/code&gt;, &lt;code&gt;stopTimer&lt;/code&gt;, &lt;code&gt;startRecording&lt;/code&gt;, &lt;code&gt;pauseRecording&lt;/code&gt;, &lt;code&gt;resumeRecording&lt;/code&gt;, &lt;code&gt;stopRecording&lt;/code&gt; live in &lt;code&gt;AssignTaskModal.tsx&lt;/code&gt; but the duplicated community suggests they need a &lt;code&gt;useRecorder.ts&lt;/code&gt; hook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Audit Zustand store adoption&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;useTaskStore&lt;/code&gt; is too isolated. Pages should be reading task state from the store rather than issuing fresh API calls on every mount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Review the proxy ↔ users API coupling&lt;/strong&gt;&lt;br&gt;
If &lt;code&gt;proxy()&lt;/code&gt; is calling &lt;code&gt;GET()&lt;/code&gt; on &lt;code&gt;/api/users&lt;/code&gt; for permission checking, that's a performance and separation-of-concerns issue. Session token claims should be enough for RBAC decisions at the middleware layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Promote &lt;code&gt;useTimeTheme&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The hook is doing real work — if the calendar uses it, more UI surfaces probably should too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The thing that surprised me most about this exercise wasn't any individual finding — it was the &lt;em&gt;density&lt;/em&gt; of the insight from purely structural data. No runtime profiling, no manual tracing. Just parse the files, extract relationships, cluster the graph.&lt;/p&gt;

&lt;p&gt;The god nodes tell you what your architecture depends on. The low-cohesion communities tell you what's doing too many jobs. The thin communities tell you what's underutilized. And the inferred edges tell you what your instincts built that the code doesn't yet make explicit.&lt;/p&gt;

&lt;p&gt;If you haven't mapped your codebase yet, do it. You'll find the library you forgot to build and the refactor that's been waiting since month two.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Next.js 15 · Prisma 7 · Neon PostgreSQL · Tailwind v4 · Zustand&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Graph: 215 nodes · 199 edges · 54 communities · 92% extraction accuracy&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Production AI Chatbot for an Educational Institute: Architecture, Lessons &amp; Full Stack Deep-Dive</title>
      <dc:creator>nitin7414</dc:creator>
      <pubDate>Sat, 23 May 2026 10:07:25 +0000</pubDate>
      <link>https://dev.to/nitin7414/building-a-production-ai-chatbot-for-an-educational-institute-architecture-lessons-full-stack-32kn</link>
      <guid>https://dev.to/nitin7414/building-a-production-ai-chatbot-for-an-educational-institute-architecture-lessons-full-stack-32kn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why This Project Exists
&lt;/h2&gt;

&lt;p&gt;Educational institutes spend enormous resources on human admissions counselors. Prospects ask the same questions repeatedly: &lt;em&gt;What courses do you offer? How long is the program? What will I earn afterwards?&lt;/em&gt; And every unanswered query at 11 PM is a lost lead.&lt;/p&gt;

&lt;p&gt;IFDA 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.&lt;/p&gt;

&lt;p&gt;This is the full technical story of how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture at a Glance
&lt;/h2&gt;

&lt;p&gt;The project is a &lt;strong&gt;Next.js 14 App Router monorepo&lt;/strong&gt; — one codebase serving the public-facing chatbot widget, the admin CRM dashboard, and all API routes. Every component was chosen deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 14 (App Router)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript throughout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Neon PostgreSQL + Prisma ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI / LLM&lt;/td&gt;
&lt;td&gt;OpenAI (GPT) for intent, embeddings, and response generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Vercel (Edge-compatible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;DoubleTick API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Custom JWT + RBAC middleware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;Server-side session store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The graph of this codebase has &lt;strong&gt;1,259 nodes&lt;/strong&gt; (files, functions, types) and &lt;strong&gt;3,276 edges&lt;/strong&gt; (calls, contains, references) distributed across &lt;strong&gt;55+ community clusters&lt;/strong&gt; — from the bot engine core, to the Prisma edge runtime, to the admin dashboard pages. Let's walk through each major system.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 1: The Bot Engine (&lt;code&gt;lib/bot/engine.ts&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The heart of the project is &lt;code&gt;processMessage()&lt;/code&gt; in &lt;code&gt;lib/bot/engine.ts&lt;/code&gt;. This single function is the traffic controller for every incoming message — whether it arrives from the web widget or WhatsApp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid Scripted + LLM Architecture
&lt;/h3&gt;

&lt;p&gt;The bot does &lt;strong&gt;not&lt;/strong&gt; send every message to GPT. That would be slow and expensive. Instead, it uses a two-tier approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scripted funnel stages&lt;/strong&gt; — structured flows for lead capture (name, phone, course interest, city) where deterministic logic is faster and more reliable than an LLM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM fallback&lt;/strong&gt; — for open-ended questions, course comparisons, career queries, or anything outside a defined stage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The stage management lives in &lt;code&gt;lib/ai/intent.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// intent.ts — simplified&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FunnelStage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMissingFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;LeadField&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getNextQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LeadField&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isLeadComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;analyzeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Analysis&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;analyzeMessage()&lt;/code&gt; is the decision function. It evaluates the message in context of the current session stage and decides whether to invoke the scripted path, call &lt;code&gt;getAIResponse()&lt;/code&gt; from &lt;code&gt;lib/ai/llm.ts&lt;/code&gt;, or trigger a carousel/quick-reply block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Message Components
&lt;/h3&gt;

&lt;p&gt;Responses are not plain strings. They're typed message blocks that the renderer layer converts to channel-specific formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/ai/llm.ts — block builders&lt;/span&gt;
&lt;span class="nf"&gt;textBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;TextBlock&lt;/span&gt;
&lt;span class="nf"&gt;domainListBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Domain&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;DomainListBlock&lt;/span&gt;
&lt;span class="nf"&gt;carouselBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CourseCarouselItem&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;CarouselBlock&lt;/span&gt;
&lt;span class="nf"&gt;courseDetailBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CourseDetailBlock&lt;/span&gt;
&lt;span class="nf"&gt;quickRepliesBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;QuickRepliesBlock&lt;/span&gt;
&lt;span class="nf"&gt;visitWebsiteBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;VisitWebsiteBlock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This block-based design means the same engine output drives both the web UI and WhatsApp — two very different rendering surfaces.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 2: Dual-Channel Rendering
&lt;/h2&gt;

&lt;p&gt;The renderer layer translates structured message blocks into channel-specific formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Renderer (&lt;code&gt;lib/bot/renderers/web.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;renderToWeb()&lt;/code&gt; converts blocks into React-compatible JSON that the frontend &lt;code&gt;StructureMessage.tsx&lt;/code&gt; component consumes. The component handles scroll behavior, carousels with &lt;code&gt;checkScroll()&lt;/code&gt;, and animated message appearance.&lt;/p&gt;

&lt;h3&gt;
  
  
  WhatsApp Renderer (&lt;code&gt;lib/bot/renderers/whatsapp.ts&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;renderToWhatsApp()&lt;/code&gt; maps 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 &lt;code&gt;lib/whatsapp/doubletick.ts&lt;/code&gt; client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// doubletick.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWhatsAppText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWhatsAppTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TemplateParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWhatsAppInteractiveList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InteractiveList&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Incoming WhatsApp messages hit &lt;code&gt;app/api/whatsapp/webhook/route.ts&lt;/code&gt;, which validates the HMAC signature via &lt;code&gt;isValidSignature()&lt;/code&gt;, parses the payload through &lt;code&gt;lib/whatsapp/messageParser.ts&lt;/code&gt;, and feeds it into the same &lt;code&gt;processMessage()&lt;/code&gt; bot engine. One engine, two channels.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 3: Course Intelligence (&lt;code&gt;lib/ai/courseData.ts&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The course catalog is the richest data source in the system. &lt;code&gt;courseData.ts&lt;/code&gt; is a comprehensive module with &lt;strong&gt;25+ exported functions&lt;/strong&gt; that the bot engine calls to build contextually accurate responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;findCourse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nf"&gt;getCourseAbout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCoursePrerequisites&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseSyllabus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseCareerOpportunities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseCareerGrowthRoadmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseProfessionalGrowthLadder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getCourseSkills&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;getAllCourseCarouselItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;CourseCarouselItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;getCarouselByIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CourseCarouselItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;getLLMCourseMap&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;LLMCourseMap&lt;/span&gt;        &lt;span class="c1"&gt;// Compact map for GPT context injection&lt;/span&gt;
&lt;span class="nf"&gt;getCourseContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;course&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="c1"&gt;// Full context string for LLM prompt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;findCourse()&lt;/code&gt; is 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getLLMCourseMap()&lt;/code&gt; is 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 4: The Knowledge Base (&lt;code&gt;lib/ai/knowledge-parser.ts&lt;/code&gt; + Embeddings)
&lt;/h2&gt;

&lt;p&gt;Beyond 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parsing Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;knowledge-parser.ts&lt;/code&gt; handles multi-format ingestion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;parseKnowledgeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ParsedKnowledge&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;parsePdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;parseCsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedRow&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="nf"&gt;parseJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedKnowledge&lt;/span&gt;
&lt;span class="nf"&gt;parseTxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedKnowledge&lt;/span&gt;
&lt;span class="nf"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Semantic Search
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/ai/embeddings.ts&lt;/code&gt; handles the vector layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="c1"&gt;// OpenAI text-embedding-3-small&lt;/span&gt;
&lt;span class="nf"&gt;embedAllFaqs&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                         &lt;span class="c1"&gt;// Bulk embed on import&lt;/span&gt;
&lt;span class="nf"&gt;embedNewFaqs&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;                         &lt;span class="c1"&gt;// Incremental update&lt;/span&gt;
&lt;span class="nf"&gt;searchSimilarFaqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FAQ&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user asks a question not covered by the scripted engine, &lt;code&gt;getRelevantKnowledge()&lt;/code&gt; fires 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 5: Lead Capture &amp;amp; CRM
&lt;/h2&gt;

&lt;p&gt;Lead capture is deeply woven into the conversation flow. As the bot collects information across multiple turns, it progressively builds a lead profile. &lt;code&gt;extractLeadInfo()&lt;/code&gt; in &lt;code&gt;intent.ts&lt;/code&gt; extracts structured data from natural language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// User says: "I'm Rohan from Lucknow, interested in the UI/UX course"&lt;/span&gt;
&lt;span class="c1"&gt;// extractLeadInfo() returns:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rohan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lucknow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;courseInterest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UI/UX Design&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verifyHumanName()&lt;/code&gt; guards against bot-abuse by rejecting implausible name strings before they reach the database.&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;isLeadComplete()&lt;/code&gt; returns true, &lt;code&gt;pushLeadToCRM()&lt;/code&gt; fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/crm/leads.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pushLeadToCRM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CompleteLead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes to the Neon PostgreSQL database via Prisma and optionally triggers a WhatsApp confirmation template to the lead's number.&lt;/p&gt;

&lt;p&gt;The admin side exposes full CRUD via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET/POST /api/admin/leads&lt;/code&gt; — list and filter leads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET/PATCH/DELETE /api/admin/leads/[id]&lt;/code&gt; — individual lead management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;AdminLeadsPage&lt;/code&gt; in &lt;code&gt;app/admin/page.tsx&lt;/code&gt; provides a CRM interface with stage tracking — counselors can move leads through the admissions funnel manually, and &lt;code&gt;updateLeadStage()&lt;/code&gt; persists the change.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 6: Appointment Scheduling
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/scheduler/calendar.ts&lt;/code&gt; handles the booking flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getAvailableDates&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AvailableDate&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;isSlotAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;bookAppointment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TimeSlot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Appointment&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When intent detection identifies scheduling intent (&lt;code&gt;handleScheduling()&lt;/code&gt; in the chatbot route), the bot presents available slots as a quick-reply carousel. The user picks one, and &lt;code&gt;bookAppointment()&lt;/code&gt; writes the appointment to the database and sends a WhatsApp confirmation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/api/schedule/route.ts&lt;/code&gt; is the public endpoint, and &lt;code&gt;app/api/admin/appointments/route.ts&lt;/code&gt; gives admins a view of all upcoming appointments.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 7: Session Management
&lt;/h2&gt;

&lt;p&gt;The bot is stateful across multiple HTTP requests. &lt;code&gt;lib/session/store.ts&lt;/code&gt; manages this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;saveSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;clearSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the client side, &lt;code&gt;app/chatbot/hooks/useChat.ts&lt;/code&gt; manages the &lt;code&gt;sessionId&lt;/code&gt; lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useChat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Generates and persists a sessionId&lt;/span&gt;
  &lt;span class="c1"&gt;// Manages message history&lt;/span&gt;
  &lt;span class="c1"&gt;// Handles loading/error states&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSessionId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session carries funnel stage, partial lead data, conversation history, and the last detected intent — everything &lt;code&gt;processMessage()&lt;/code&gt; needs to pick up exactly where the conversation left off.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 8: Admin Dashboard &amp;amp; RBAC
&lt;/h2&gt;

&lt;p&gt;The admin panel is a full internal application with multiple pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Functionality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Analytics charts (BarChart), metrics overview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/dashboard/members&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Team member management (add, edit, remove)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lead CRM with stage management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/conversations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full chat history viewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/knowledge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Knowledge file upload/management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/templates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WhatsApp template builder with AI generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/admin/profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Profile management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  RBAC Implementation
&lt;/h3&gt;

&lt;p&gt;The permission system uses a &lt;strong&gt;role-permission matrix&lt;/strong&gt; architecture, documented in &lt;code&gt;implementation_plan.md&lt;/code&gt; and implemented in &lt;code&gt;lib/auth/permissions.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDefaultPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminRole&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middleware chain enforces this at the API level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/auth/middleware.ts&lt;/span&gt;
&lt;span class="nf"&gt;requireAdmin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// Verifies JWT, rejects unauthenticated requests&lt;/span&gt;
&lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminRole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Enforces role-based access&lt;/span&gt;
&lt;span class="nf"&gt;getAdminFromRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// Extracts admin context from token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lib/auth/jwt.ts&lt;/code&gt; handles token lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;signToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JWTPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;JWTPayload&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nf"&gt;verifyTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;JWTPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sidebar in the admin UI uses &lt;code&gt;hasPermission()&lt;/code&gt; to filter navigation items — counselors see leads and conversations; superadmins see everything including member management and analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  WhatsApp Template Builder
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/admin/templates&lt;/code&gt; page deserves special mention. It's an AI-powered editor where admins compose multi-slide WhatsApp broadcast campaigns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;generateWithAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// GPT generates slide content from a prompt&lt;/span&gt;
&lt;span class="nf"&gt;addSlide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;removeSlide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// Manage campaign structure&lt;/span&gt;
&lt;span class="nf"&gt;buildWhatsAppText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// Compile slides to WhatsApp text format&lt;/span&gt;
&lt;span class="nf"&gt;buildHTMLBlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// Compile to HTML preview&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admins 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  System 9: Analytics
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app/api/admin/analytics/route.ts&lt;/code&gt; aggregates metrics from the database — lead conversion rates, conversation volumes, popular courses, appointment completion rates. The dashboard &lt;code&gt;BarChart&lt;/code&gt; component visualizes these in real time for the admin team.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;types/analytics.ts&lt;/code&gt; defines the typed response schema, and &lt;code&gt;prisma/scripts/testAnalytics.ts&lt;/code&gt; was used during development to seed realistic test data and validate aggregation queries.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Database Schema
&lt;/h2&gt;

&lt;p&gt;All data flows through Prisma ORM on Neon PostgreSQL. Key models include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admin&lt;/strong&gt; — staff accounts with role and permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead&lt;/strong&gt; — prospect data with funnel stage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation / Message&lt;/strong&gt; — full chat history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KnowledgeFile&lt;/strong&gt; — uploaded knowledge documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAQ / FAQEmbedding&lt;/strong&gt; — vector-ready FAQ storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appointment&lt;/strong&gt; — scheduled counseling sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsAppMessage&lt;/strong&gt; — outbound message log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AnalyticsEvent&lt;/strong&gt; — event-level analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prisma generates a full Edge-compatible client (&lt;code&gt;generated/prisma/&lt;/code&gt;) for Vercel's Edge Runtime, enabling low-latency database queries from serverless functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Engineering Challenges &amp;amp; How I Solved Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. GPT Conversation History Corruption
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Separated session state from conversation history. The &lt;code&gt;messages&lt;/code&gt; array sent to GPT contains only actual user/assistant turns. Lead context and session state are injected exclusively in the system prompt, rebuilt fresh on every request from &lt;code&gt;getSession()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Edge Runtime + Prisma
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Prisma's standard Node.js client uses modules incompatible with Vercel's Edge Runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Used Prisma's WASM-based edge client (&lt;code&gt;generated/prisma/edge.js&lt;/code&gt;) with the &lt;code&gt;wasm-compiler-edge.js&lt;/code&gt; adapter. This required careful configuration in &lt;code&gt;prisma.config.ts&lt;/code&gt; and &lt;code&gt;next.config.ts&lt;/code&gt; to bundle correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Course Carousel → Bot Agent Wiring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; When a user clicked a course card in the carousel, the click event needed to trigger a bot message as if the user had typed the course name — but the component boundaries between &lt;code&gt;CourseCarousel.tsx&lt;/code&gt; and &lt;code&gt;useChat.ts&lt;/code&gt; made this tricky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Exposed a &lt;code&gt;sendMessage()&lt;/code&gt; method from &lt;code&gt;useChat()&lt;/code&gt; and threaded it down as a prop to &lt;code&gt;CourseCarousel&lt;/code&gt;. Carousel card clicks call &lt;code&gt;sendMessage(course.name)&lt;/code&gt; directly, entering the course into the conversation and triggering the full bot response pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Suggestion Buttons During Lead Capture
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; During lead capture (collecting name, city, etc.), users would go off-script. Raw free text answers led to extraction failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;code&gt;getSuggestionsForNextField()&lt;/code&gt; in the chatbot route generates contextual quick-reply buttons for each lead field. If collecting city, the suggestions show major Indian cities. If collecting course interest, they show relevant domains. Users can tap or type — both paths work.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. WhatsApp Webhook Signature Validation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Public webhooks are targets for spoofed requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;code&gt;isValidSignature()&lt;/code&gt; validates HMAC-SHA256 signatures on every incoming WhatsApp webhook payload using the DoubleTick shared secret. Invalid signatures are rejected with &lt;code&gt;403&lt;/code&gt; before any processing occurs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure Overview (Though it's a private repository)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ifda/
├── app/
│   ├── page.tsx                    # Public landing + chat widget
│   ├── layout.tsx                  # Root layout
│   ├── api/
│   │   ├── chatbot/route.ts        # Core bot API
│   │   ├── whatsapp/
│   │   │   ├── webhook/route.ts    # Incoming WhatsApp
│   │   │   └── send/route.ts       # Outbound WhatsApp
│   │   ├── schedule/route.ts       # Appointment booking
│   │   ├── faq/route.ts
│   │   ├── embed/route.ts
│   │   └── admin/                  # Protected admin APIs
│   ├── chatbot/
│   │   ├── components/
│   │   │   ├── ChatWindow.tsx
│   │   │   ├── InputBox.tsx
│   │   │   ├── CourseCaraousel.tsx
│   │   │   └── StructureMessage.tsx
│   │   └── hooks/useChat.ts
│   └── admin/                      # Admin CRM pages
│       ├── login/
│       ├── dashboard/
│       ├── conversations/
│       ├── knowledge/
│       ├── templates/
│       └── profile/
├── lib/
│   ├── ai/
│   │   ├── courseData.ts           # Course intelligence
│   │   ├── embeddings.ts           # Vector search
│   │   ├── intent.ts               # Intent + lead extraction
│   │   ├── knowledge-parser.ts     # Multi-format doc parsing
│   │   └── llm.ts                  # LLM client + block builders
│   ├── auth/
│   │   ├── jwt.ts
│   │   ├── middleware.ts
│   │   └── permissions.ts          # RBAC
│   ├── bot/
│   │   ├── engine.ts               # Core processMessage()
│   │   └── renderers/
│   │       ├── web.ts
│   │       └── whatsapp.ts
│   ├── crm/leads.ts
│   ├── db/
│   │   ├── prisma.ts
│   │   └── queries.ts
│   ├── scheduler/calendar.ts
│   ├── session/store.ts
│   └── whatsapp/
│       ├── doubletick.ts
│       ├── messageParser.ts
│       └── types.ts
├── config/
│   ├── constants.ts
│   └── prompts.ts                  # All LLM system prompts
├── types/
│   ├── chat.ts
│   ├── lead.ts
│   └── analytics.ts
└── prisma/
    ├── schema.prisma
    └── scripts/                    # Seed + test scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge-first:&lt;/strong&gt; All API routes are Edge-compatible. Vercel deploys them to the closest edge node, cutting latency for Indian users significantly vs. a single-region serverless function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session caching:&lt;/strong&gt; &lt;code&gt;getSession()&lt;/code&gt; uses a lightweight in-memory cache layer before hitting PostgreSQL, reducing DB round-trips on sequential messages in an active conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM cost control:&lt;/strong&gt; The scripted funnel handles the majority of conversations. GPT is only invoked when the intent engine genuinely can't determine the next step — keeping API costs predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding on ingest:&lt;/strong&gt; Knowledge files are embedded at upload time, not at query time. Search is a fast vector similarity lookup, not a synchronous embedding + search chain.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Extract the bot engine into a proper state machine.&lt;/strong&gt; &lt;code&gt;processMessage()&lt;/code&gt; is powerful but has grown organically. Formalizing it as an explicit state machine (with states like &lt;code&gt;COLLECTING_NAME&lt;/code&gt;, &lt;code&gt;COLLECTING_CITY&lt;/code&gt;, &lt;code&gt;COURSE_DISCOVERY&lt;/code&gt;, &lt;code&gt;SCHEDULING&lt;/code&gt;) would make the flow easier to reason about and test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Add streaming responses.&lt;/strong&gt; The current implementation waits for the complete LLM response before sending. Server-Sent Events or the Vercel AI SDK's streaming primitives would make the bot feel dramatically faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Separate the WhatsApp and web sessions more cleanly.&lt;/strong&gt; Right now, both channels share the same session store schema. As WhatsApp-specific flows diverge (templates, interactive lists, 24-hour messaging windows), having a channel-aware session type would reduce the number of &lt;code&gt;if (channel === 'whatsapp')&lt;/code&gt; checks in the engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Add a proper evaluation pipeline.&lt;/strong&gt; Testing chatbot quality is hard. A golden-dataset evaluation pipeline — where known inputs are run through the bot and outputs are checked against expected responses — would catch regressions when the LLM or prompt changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The bot handles the full admissions journey end-to-end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Course discovery&lt;/strong&gt; — carousel-driven browsing + natural language course queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead qualification&lt;/strong&gt; — multi-turn lead capture with field-level validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge search&lt;/strong&gt; — semantic search over admin-uploaded documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appointment booking&lt;/strong&gt; — slot selection and WhatsApp confirmation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staff handoff&lt;/strong&gt; — completed leads surfaced in the admin CRM with full conversation history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast campaigns&lt;/strong&gt; — AI-assisted WhatsApp template creation and dispatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The admin team went from managing a static form to having a live CRM where every lead arrives pre-qualified with course interest, location, and contact details captured by the bot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 14&lt;/strong&gt; (App Router, Edge Runtime)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (strict mode throughout)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prisma ORM&lt;/strong&gt; + &lt;strong&gt;Neon PostgreSQL&lt;/strong&gt; (WASM edge client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI API&lt;/strong&gt; (GPT-4o for intent/generation, text-embedding-3-small for semantic search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DoubleTick&lt;/strong&gt; (WhatsApp Business API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; (deployment, edge functions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT&lt;/strong&gt; (stateless admin auth)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; (admin UI and chat widget)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The most valuable architecture decision was the &lt;strong&gt;block-based message system&lt;/strong&gt;. By never treating bot responses as raw strings, every part of the system — the engine, the renderers, the frontend components — operates on typed, predictable data structures. Adding a new message type (say, a booking confirmation card) means adding one block builder in &lt;code&gt;llm.ts&lt;/code&gt;, one render case in each renderer, and one React component. The engine doesn't change.&lt;/p&gt;

&lt;p&gt;The second best decision was &lt;strong&gt;keeping the LLM in the hot path as little as possible&lt;/strong&gt;. GPT is powerful but introduces latency and cost. Using it for intent classification and open-ended generation, while handling the structured lead capture funnel with deterministic code, gives the best of both worlds — intelligent conversation where it matters, reliability and speed where it counts.&lt;/p&gt;

&lt;p&gt;If you're building something similar or have questions about any of the systems described here, drop them in the comments. Happy to go deeper on any piece of this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for IFDA — Indian design and technology education institute.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
