Skip to main content

Built withNext.js 16, LangGraph and PostgreSQL (pgvector)

AI support that answers from your docs, and hands the chat to a person when it can't.

An AI customer support platform built on Next.js 16 App Router, LangGraph state graphs, PostgreSQL with pgvector for document search, and Fastify 5 WebSockets. Human handoff is enforced on the server.

Execution inspectorSample data
Node: intent_router

Input: "How do I configure vector cosine similarity search?"

Classification: needsRetrieval = true | handoffRequested = false

Node: rag_retriever

Querying PostgreSQL 16 + vector table "KnowledgeDocument"

doc_04_pgvector_setup.md(cos_sim: 0.941)doc_12_langgraph_nodes.md(cos_sim: 0.887)
Node: llm_synthesizer

Model: Hugging Face (Qwen2.5-Instruct / OpenRouter)

Tokens: 412 prompt / 148 completion · Streaming via Socket.IO room session_9f82

Node: guardrail_validator

Personal data check passed · Safety rules passed · Answer matches the retrieved documents

See the LangGraph steps for one question

Pick a scenario and replay it: intent routing, document search, answer generation and the server-side guardrails.

Step-by-step simulator

Scripted demo, sample data

Pick a scenario to see which LangGraph step handles it.

Customer question

How does the platform handle vector cosine similarity search and embedding storage?

Node 1: intent classifier0.8ms

needsRetrieval: true | handoffRequested: false

Node 2: document retriever14.2ms
docs/04-vector-setup.md94.5% match
packages/db/prisma/schema.prisma89.2% match
Node 3: answer generation and streaming120.5ms

Hugging Face / OpenRouter (qwen2.5-0.5b-instruct) · 284 tokens

Node 4: guardrail checkPassed

Personal data: None found · Flag: NONE

ResultAnswered by the AI
The platform provisions PostgreSQL 16 with the vector extension enabled. Documents ingested in /admin/knowledge are chunked and vectorized into 384/1536-dim embeddings. During retrieval, LangGraph executes raw SQL cosine distance queries (1 - (embedding <=> $1)) with a configurable similarity threshold (default >0.65) to inject ground truth into prompt templates.

How the platform is built

Pick a part of the system to see how it works.

State graph

A graph of fixed steps for every answer

The platform uses a LangGraph StateGraph to move each question through explicit steps: intent classification, document retrieval, answer generation, and guardrails.

  • Typed state graph with immutable context snapshots
  • Quick path for conversational greetings
  • Fallback to another provider when one fails
  • Execution telemetry logged per node

Technical details

Pipeline steps
4 core nodes
Orchestration
LangGraph

System layers

From the chat window, through the Fastify gateway, to the LangGraph steps and PostgreSQL. Select a layer to see its details.

LangGraph workflow (packages/agent)StateGraph
LangGraph node 1

LangGraph Intent Classifier Node

Intent triage and handoff detection

What it does

Detects plain greetings (skipping document search to save compute) and flags explicit requests for a person before any search runs.

Technology

LangGraph StateGraphDeterministic Regex & Heuristic Routing

Simplified source excerpt

// packages/agent/src/workflow/nodes.ts
export function classifyIntent(query: string): RouteResult {
  const isPureGreeting = PURE_GREETING_RE.test(query.trim());
  const handoffRequested = HANDOFF_RE.test(query.trim());
  return { needsRetrieval: !isPureGreeting, handoffRequested };
}
Scripted demo, sample data

Follow a message through the system

Step through five scenarios: a document question, a handoff to a person, a blocked prompt injection, a greeting, and a provider outage.

Scenario:1. Answer from your documentsStep 1 of 10
Query: "How do I configure vector cosine similarity search and embedding storage?"
Current stage:Customer Client DispatchInput
Flowchart
On this path Not used
1. Message intake and gateway
#1
1. Customer message
WebSocket Client (/chat)
#2
2. Fastify Gateway
Auth, Sessions & Prisma
2. LangGraph intent triage and context
#3
3. Intent Classifier
LangGraph Route Node
#4
4. Memory Context
Customer Profile & History
3. Document search and answer generation
#5
5. Pre-LLM Guardrail
Injection & Threat Filter
#6
6. Document search
PostgreSQL 16 Cosine Match
#7
7. LLM Engine
Hugging Face / OpenRouter
8. Provider fallback
OpenRouter / Echo Fallback
4. Guardrails and server policy checks
#8
9. Post-Guardrail
PII Scrubbing & Sanitizer
#9
10. Handoff Policy
Server Policy Enforcement
5. Where the conversation ends up
11. Live admin queue
Human Agent Takeover
12. Security abort
Stopped before the model
#10
13. Streamed reply
Socket.IO Client Response
Current step~4ms

Customer Client Dispatch

User dispatches query payload over WebSocket connection (/chat namespace).

LangGraph state changeSample
{
  "query": "How do I configure vector cosine similarity search...",
  "customerId": "cust_981"
}
Event log (sample)
About this path

Full RAG pipeline: intent triage ➔ memory lookup ➔ cosine search ➔ grounded answer ➔ guardrail check.

Simplified code excerpts

Shortened versions of the LangGraph workflow, Prisma schema, Fastify server and Docker Compose setup. The full source is in the GitHub repository.

LangGraphIntent classification router, grounding prompt synthesis, and regex guardrails.
Simplified from packages/agent/src/workflow/nodes.ts
import { ChatMessage, RetrievedDocument } from '../providers/types';

export function classifyIntent(query: string): RouteResult {
  const trimmed = query.trim();
  const isPureGreeting = PURE_GREETING_RE.test(trimmed);
  const needsRetrieval = !isPureGreeting;
  const handoffRequested = HANDOFF_RE.test(trimmed);
  return { needsRetrieval, handoffRequested };
}

export function buildSystemPrompt(
  template: string,
  systemName: string,
  docs: RetrievedDocument[],
  memoryContext = '',
): string {
  let system = template.replace(/\{SYSTEM_NAME\}/g, systemName);
  if (docs.length > 0) {
    const context = docs
      .map((d) => `${d.title}\n${d.content}`)
      .join('\n---\n')
      .slice(0, 12000);
    system += `\n\n[CONTEXT]\n${context}\n[/CONTEXT]`;
  }
  return system;
}

export function sanitizeAnswer(answer: string): string {
  const cleaned = answer.trim();
  for (const pattern of BANNED_PATTERNS) {
    if (pattern.test(cleaned)) return GUARDRAIL_FALLBACK;
  }
  return cleaned.slice(0, 8000);
} 
Self-hosting

Run it on your own machine

Docker Compose, a database push, and the dev server: three commands to a running instance.

Local setup (Bash / PowerShell)Docker + Bun
  1. # 1. Start PostgreSQL and Redis
    docker compose up -d

  2. # 2. Push the database schema and seed test data
    bun run db:push && bun run db:seed

  3. # 3. Start the Fastify API (:4000) and Next.js web app (:3000)
    bun run dev

Role-based accessJWT + bcrypt

  • AdministratorRole: ADMIN

    Manages LLM providers, knowledge base documents, guardrails and audit logs.

  • Support agentRole: AGENT

    Works the live queue, takes over conversations from the AI, and resolves them.

  • CustomerRole: USER

    Chats with the AI over Socket.IO and is handed to a person when needed.