Transform documents into intelligent knowledge with AI-powered semantic search and graph visualization
Documind is a document intelligence platform that transforms uploaded files into an interactive, searchable knowledge base. Upload documents, ask questions in natural language, and get answers grounded in β and cited to β your own files, while a second AI pipeline builds a queryable knowledge graph of the entities and relationships hiding across the whole collection.
Most "chat with your PDF" tools stop at naive RAG: embed some chunks, retrieve the nearest neighbors, paste them into a prompt. That falls apart the moment a question spans multiple documents or depends on how entities relate to each other β a plain vector search has no notion of "this person also appears in that other contract" or "these two reports are about the same acquisition."
Documind exists to answer a narrower, harder question: can a RAG system also understand the structure connecting a document collection, not just its text? That meant building two AI subsystems on top of the same ingestion pipeline:
- A retrieval-augmented generation pipeline for grounded, cited natural-language Q&A over a user's documents.
- A knowledge-graph extraction pipeline that runs entity recognition, cross-document entity resolution, semantic clustering, document similarity, and topic modeling on the same chunks β turning a flat pile of files into a queryable graph of who/what/where and how they connect, rendered as an interactive visualization.
Both pipelines had to run identically against a hosted LLM (OpenAI) or a fully local one (Ollama) with zero code branching at the call sites, and the whole stack had to run for free on a laptop via Docker β so the project doubled as an exercise in designing backend-agnostic infrastructure abstractions (AI client, storage client, database clients) that don't leak their provider into application code. See AI Systems Deep Dive below for how each pipeline actually works, with the real thresholds and models involved.
- π€ AI-Powered Q&A: Ask questions in natural language and get intelligent answers with source citations
- π Interactive Knowledge Graph: Visualize relationships between entities with advanced filtering and layout options
- π Semantic Search: Find relevant information using vector-based similarity search
- π Multi-Format Support: Process PDFs, Word documents, and text files seamlessly
- π Secure & Private: Complete user data isolation with enterprise-grade security
- β‘ Real-time Processing: Background document processing with live status updates
- ποΈ Smart Filtering: Customizable graph views with entity type filters and confidence thresholds
- π§ Resilient Architecture: Graceful error handling with fallback options for all services
| Hero | Feature grid | Process & CTA |
|---|---|---|
![]() |
![]() |
![]() |
| Sign up | Sign in |
|---|---|
![]() |
![]() |
| Overview | Account menu |
|---|---|
![]() |
![]() |
| Empty state | Question asked | Answer with cited sources |
|---|---|---|
![]() |
![]() |
![]() |
| Overview | Legend | Filters panel | Node detail |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
This is the section worth reading closely if you're evaluating the AI/ML engineering rather than the product. Every claim below is backed by code β file paths point to the actual implementation, not a design doc.
Ingestion β runs once per uploaded document:
flowchart TD
A["Raw file (PDF / DOCX / TXT)"] -->|"LangChain PDFLoader Β· mammoth.js Β· plain text"| B["Extracted text + metadata<br/>title, author, page count"]
B -->|"whitespace normalization<br/>control-char stripping"| C["Preprocessed text"]
C -->|"lib/ai/processing.ts :: chunkText()"| D["Sliding-window chunks<br/>500 words/chunk, 50-word overlap"]
D -->|"batched, 100 chunks/call<br/>rate-limit delay"| E["Embeddings<br/>Ollama nomic-embed-text (768-dim)<br/>or OpenAI text-embedding-3-small (1536-dim)"]
E --> F[("Qdrant upsert<br/>cosine distance<br/>indexed on userId + docId")]
Query β runs on every chat message:
flowchart TD
A["User question"] -->|"embedded with the ingestion-time model"| B["Query embedding"]
B --> C[("Qdrant ANN search<br/>filtered to userId (+ docId)<br/>score β₯ 0.2, top-k = 10")]
C --> D["Prompt assembly<br/>numbered, source-labeled context blocks<br/>'[Source 1 - filename.pdf]: chunk text'"]
D -->|"temperature 0.3"| E["LLM completion<br/>answers only from context, cites sources,<br/>admits when context is insufficient"]
E --> F["Answer + ranked sources + confidence score<br/>0.8 Γ avg retrieval score + 0.2 if answer > 50 chars, capped at 1.0"]
The user-scoping filter is applied inside the Qdrant query (lib/db/qdrant.ts :: searchVectors) via a payload-indexed must clause on userId, not as a post-fetch filter in application code β one user's documents are never in another user's candidate set, even before a similarity score is computed. See lib/ai/chat.ts for the full retrieval β prompt β generation flow.
The same chunks feed a second, independent pipeline that builds a Neo4j graph instead of (or alongside) the vector index. It runs as five stages, implemented in lib/ai/entities.ts, lib/ai/document-similarity.ts, and lib/ai/topic-modeling.ts:
flowchart LR
A["1. Entity extraction<br/>LLM NER per chunk"] --> B["2. Co-occurrence linking<br/>COOCCURS_WITH edges"]
B --> C["3. Cross-document resolution<br/>SAME_AS / SIMILAR_TO"]
C --> D["4. Semantic clustering<br/>category-specific heuristics"]
D --> E["5. Similarity & topics<br/>DOCUMENT_SIMILAR_TO + LLM topic modeling"]
| Stage | What it does | Signal / threshold |
|---|---|---|
| 1. Entity extraction | Per-chunk LLM call (temperature 0.1) returns structured JSON: named entities (PERSON / ORGANIZATION / LOCATION / DATE / MONEY / OTHER) with confidence scores, plus explicit relationships between them |
LLM structured output, capped at 10 entities/chunk |
| 2. Co-occurrence linking | Every entity pair found in the same chunk gets a COOCCURS_WITH edge, weighted by combined entity confidence and how close together the two mentions appear in the raw text |
Character-distance proximity bonus |
| 3. Cross-document resolution | New entities are compared against a user's existing entities of the same category: exact-name match, Jaccard word overlap, person-name subset matching ("J. Smith" β "John Smith"), organization-abbreviation containment | SAME_AS above 0.8 similarity, SIMILAR_TO between 0.6β0.8 |
| 4. Semantic clustering | Category-specific heuristics group related entities β shared professional titles for people, shared industry terms for organizations, geographic containment for locations β plus cross-category inference (WORKS_AT, LOCATED_IN) derived from co-occurrence |
SIMILAR_TO between 0.4β0.8 |
| 5. Document similarity & topics | Per-document embeddings (mean-pooled from that document's chunk vectors) are compared pairwise by cosine similarity for DOCUMENT_SIMILAR_TO edges; a separate LLM call clusters the whole collection into topics and assigns documents to them |
Cosine similarity > 0.3 |
Stages 3β5 are triggered on demand from the graph UI ("Cluster Entities" / "Analyze Doc Similarity" / "Extract Topics") rather than automatically on every upload, since they're collection-wide operations β re-running them after adding a new document lets the graph re-resolve entities against the whole corpus, not just the new file.
The result renders with Cytoscape.js: node size scales with degree (how connected an entity is), edge styling varies by relationship type (structural MENTIONS/CONTAINS edges recede into the background; semantic edges stay prominent), and every relationship carries its confidence score as graph data so results can be filtered by it.
Every AI call in the app β chat completion, embeddings, entity extraction, topic modeling β goes through a single factory (lib/ai/client.ts) built on the openai SDK:
export function getOpenAIClient(): OpenAI {
if (aiMode === "local") {
return new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "ollama" });
}
return new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}Ollama speaks the OpenAI /v1 API, so switching AI_MODE between local and cloud is a config change, not a code change β no call site knows or cares which provider it's talking to. The same pattern extends to storage (MinIO/S3), the vector DB (Qdrant local/cloud), the graph DB (Neo4j local/Aura), and auth (Auth.js/Clerk) β each independently toggleable, all through one docker-compose.yml with per-service Compose profiles (see One Compose File, Only What You Need below). That lets the whole stack run for $0 on a laptop during development while staying a single env var away from a fully managed cloud deployment.
Documind employs a sophisticated multi-database architecture designed for scalability and performance:
flowchart TB
FE["Frontend<br/>Next.js 15 (App Router) Β· React 19 Β· TypeScript<br/>Tailwind CSS v4 Β· Radix UI Β· Cytoscape.js<br/>Clerk or Auth.js (AUTH_MODE)"]
BE["Backend<br/>Next.js API Routes Β· Auth middleware<br/>AI processing pipeline<br/>File processing (LangChain / mammoth.js)"]
ST[("File Storage<br/>MinIO / AWS S3<br/>Presigned URLs")]
AI["AI Services<br/>Ollama or OpenAI<br/>Embeddings Β· Chat Β· Entity Extraction"]
MG[("MongoDB")]
QD[("Qdrant")]
N4[("Neo4j")]
FE -->|API Routes| BE
BE --> ST
BE --> AI
BE --> MG
BE --> QD
BE --> N4
flowchart LR
subgraph MongoDB["MongoDB"]
M1["Documents metadata"]
M2["User data"]
M3["Processing status"]
M4["File references"]
end
subgraph Qdrant["Qdrant"]
Q1["Vector embeddings"]
Q2["Semantic search"]
Q3["Similarity matching"]
end
subgraph Neo4j["Neo4j"]
N1["Knowledge graph"]
N2["Entities & relations"]
N3["Topics & clusters"]
end
- Next.js 15: React framework with App Router
- TypeScript: Type-safe development
- Tailwind CSS v4: Modern utility-first styling
- Radix UI: Accessible component primitives
- React Hot Toast: User notifications
- Cytoscape.js: Interactive graph visualization
- Next.js API Routes: RESTful endpoints
- Clerk: Authentication and user management
- OpenAI SDK: Embeddings, chat completion, and entity extraction β points at OpenAI or a local Ollama server (OpenAI-compatible
/v1API), depending onAI_MODE
- Mammoth.js: Word document processing
- LangChain (
@langchain/communityPDFLoader): PDF text extraction - Text Chunking: Intelligent content segmentation
- Entity Extraction: NER with relationship mapping
flowchart TD
A["1. User Authentication<br/>Clerk or Auth.js"] --> B["2. File Upload<br/>MinIO / AWS S3"]
B --> C["3. Background Processing"]
C --> C1["Text extraction"]
C --> C2["AI analysis (Ollama / OpenAI)"]
C --> C3["Vector generation (Qdrant)"]
C --> C4["Entity extraction (Neo4j)"]
C --> C5["Metadata storage (MongoDB)"]
C1 & C2 & C3 & C4 & C5 --> D["4. Real-time Status Updates"]
D --> E["5. Interactive Features"]
E --> E1["Semantic search"]
E --> E2["AI chat"]
E --> E3["Graph visualization"]
E --> E4["Document management"]
flowchart LR
A["File Upload<br/>MinIO / AWS S3<br/>Presigned URLs"] --> B["Text Extraction<br/>mammoth.js (Word)<br/>LangChain PDFLoader (PDF)"]
B --> C["AI Processing<br/>Ollama / OpenAI<br/>Entity recognition Β· Topic modeling"]
C --> D[("Vector embeddings<br/>Qdrant")]
C --> E[("Entity extraction<br/>Neo4j")]
C --> F[("Metadata storage<br/>MongoDB")]
Before running Documind, ensure you have:
- Node.js (v18 or higher)
- npm or yarn package manager
- Docker Desktop (for running any services set to
localmode β see below) - MongoDB instance (local Docker or MongoDB Atlas)
- Qdrant vector database (local Docker or Qdrant Cloud)
- Neo4j graph database (local Docker or Neo4j Aura)
- File storage (local Docker MinIO or AWS S3)
- AI provider (local Docker Ollama or OpenAI API key)
- Authentication (self-hosted email/password β no external account needed, or a Clerk account for cloud mode)
All five backing services live in a single docker-compose.yml, each tagged with a Compose profile (mongodb, qdrant, neo4j, storage, ai) matching its *_MODE variable. Running docker compose up -d with no profiles starts nothing β you activate only the services you're actually running locally.
Rather than tracking which profiles to pass by hand, npm run docker:up reads .env.local, checks each *_MODE variable, and starts (via docker compose --profile <x> ... up -d) only the containers whose mode is local β any service set to cloud is left alone:
npm run docker:up # starts only the services whose *_MODE=local in .env.local
npm run docker:down # stops the same setFor example, with QDRANT_MODE=local and everything else cloud, npm run docker:up runs the equivalent of docker compose --profile qdrant up -d β only the Qdrant container starts. You can still target profiles manually if you prefer: docker compose --profile mongodb --profile qdrant up -d.
AUTH_MODE is a sixth toggle following the same local/cloud convention, but it has no Compose profile β local auth is a library (Auth.js), not a service, so npm run docker:up never starts a container for it.
git clone https://github.com/kanugurajesh/DocuMind.git
cd DocuMindnpm install --legacy-peer-depsNote:
--legacy-peer-depsis required because@langchain/community(used for PDF parsing) declares a peer dependency conflict with theopenaiSDK version this project uses.
Copy the example environment file and configure your services:
cp .env.example .env.localUpdate .env.local with your service credentials:
# Authentication
# Mode: "local" (self-hosted email/password via Auth.js, stored in MongoDB) | "cloud" (uses Clerk)
AUTH_MODE=local
NEXT_PUBLIC_AUTH_MODE=local
# Generate with: npx auth secret
AUTH_SECRET=generate_with_npx_auth_secret
# Clerk Authentication (used when AUTH_MODE=cloud)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key
CLERK_SECRET_KEY=sk_test_your_key
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
# MongoDB Configuration
# Mode: "local" (Docker Desktop, mongodb://localhost:27017/documind) | "cloud" (uses MONGODB_URI)
MONGODB_MODE=local
MONGODB_LOCAL_URI=mongodb://localhost:27017/documind
MONGODB_URI=mongodb+srv://user:password@your-cluster.mongodb.net/documind
MONGODB_DB_NAME=documind
# Qdrant Vector Database
# Mode: "local" (Docker Desktop, http://localhost:6333) | "cloud" (uses QDRANT_URL + QDRANT_API_KEY)
QDRANT_MODE=local
QDRANT_URL=https://your-instance.cloud.qdrant.io:6333
QDRANT_API_KEY=your_qdrant_api_key
# Neo4j Graph Database
# Mode: "local" (Docker Desktop, bolt://localhost:7687) | "cloud" (uses NEO4J_URI + credentials)
NEO4J_MODE=local
NEO4J_LOCAL_PASSWORD=neo4j
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your_neo4j_password
# File Storage
# Mode: "local" (Docker Desktop MinIO, http://localhost:9000) | "cloud" (uses AWS S3)
STORAGE_MODE=local
STORAGE_LOCAL_ENDPOINT=http://localhost:9000
STORAGE_LOCAL_ACCESS_KEY=minioadmin
STORAGE_LOCAL_SECRET_KEY=minioadmin
STORAGE_LOCAL_BUCKET_NAME=documind
# AWS S3 Storage (used when STORAGE_MODE=cloud)
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_aws_access_key_id
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
AWS_S3_BUCKET_NAME=your-bucket-name
# AI / LLM Configuration
# Mode: "local" (Docker Desktop Ollama, http://localhost:11434) | "cloud" (uses OpenAI)
AI_MODE=local
OLLAMA_BASE_URL=http://localhost:11434/v1
OLLAMA_CHAT_MODEL=llama3.1
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
# OpenAI API (used when AI_MODE=cloud)
OPENAI_API_KEY=sk-your_openai_key
OPENAI_MODEL=gpt-4o-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
# Application Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3000
MAX_FILE_SIZE_MB=10
MAX_CHUNK_SIZE=500
# Must match the active embedding model's output size.
# Ollama nomic-embed-text = 768 | OpenAI text-embedding-3-small = 1536
EMBEDDING_DIMENSIONS=768Ensure all databases are running and accessible:
MongoDB supports two modes controlled by MONGODB_MODE in .env.local.
Local (Docker Desktop) β recommended for development:
Set in .env.local:
MONGODB_MODE=local
MONGODB_LOCAL_URI=mongodb://localhost:27017/documindThen start the container (or docker compose --profile mongodb up -d directly):
npm run docker:upCloud (MongoDB Atlas):
- Create a free cluster at cloud.mongodb.com
- Copy the connection string and add your IP to the access list
Set in .env.local:
MONGODB_MODE=cloud
MONGODB_URI=mongodb+srv://user:password@your-cluster.mongodb.net/documindQdrant supports two modes controlled by QDRANT_MODE in .env.local.
Local (Docker Desktop) β recommended for development:
Set in .env.local:
QDRANT_MODE=localThen start the container (or docker compose --profile qdrant up -d directly) and verify it's running:
npm run docker:up
curl http://localhost:6333/healthzCloud (Qdrant Cloud):
- Create a free cluster at cloud.qdrant.io
- Copy the cluster URL and API key
Set in .env.local:
QDRANT_MODE=cloud
QDRANT_URL=https://your-instance.cloud.qdrant.io:6333
QDRANT_API_KEY=your_qdrant_api_keyNeo4j supports two modes controlled by NEO4J_MODE in .env.local.
Local (Docker Desktop) β recommended for development:
Set in .env.local:
NEO4J_MODE=local
NEO4J_LOCAL_PASSWORD=neo4j # must match the neo4j service's NEO4J_AUTH in docker-compose.ymlThen start the container (or docker compose --profile neo4j up -d directly) and open the browser UI (login: neo4j / neo4j):
npm run docker:up
# http://localhost:7474Note: On first login Neo4j may prompt you to change the default password. If you do, update
NEO4J_LOCAL_PASSWORDto match and restart the dev server.
Cloud (Neo4j Aura):
- Create a free instance at console.neo4j.io
- Copy the connection URI, username, and password
Set in .env.local:
NEO4J_MODE=cloud
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your_neo4j_passwordFile storage supports two modes controlled by STORAGE_MODE in .env.local.
Local (Docker Desktop MinIO) β recommended for development:
Set in .env.local:
STORAGE_MODE=local
STORAGE_LOCAL_ENDPOINT=http://localhost:9000
STORAGE_LOCAL_ACCESS_KEY=minioadmin
STORAGE_LOCAL_SECRET_KEY=minioadmin
STORAGE_LOCAL_BUCKET_NAME=documindThen start the container (or docker compose --profile storage up -d directly) and open the browser console (login: minioadmin / minioadmin):
npm run docker:up
# http://localhost:9001The bucket is created automatically on first run if it doesn't exist.
Cloud (AWS S3):
- Create an S3 bucket in your AWS account
- Create an IAM user/role with read/write access to that bucket
Set in .env.local:
STORAGE_MODE=cloud
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_aws_access_key_id
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
AWS_S3_BUCKET_NAME=your-bucket-nameChat, embeddings, and entity/topic extraction all go through a single openai SDK client (lib/ai/client.ts) that supports two modes controlled by AI_MODE in .env.local. Ollama exposes an OpenAI-compatible /v1 API, so no separate SDK is needed for local mode.
Local (Docker Desktop Ollama) β recommended for development, no API costs:
Set in .env.local:
AI_MODE=local
OLLAMA_BASE_URL=http://localhost:11434/v1
OLLAMA_CHAT_MODEL=llama3.1
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
EMBEDDING_DIMENSIONS=768Then start the container (or docker compose --profile ai up -d directly) and pull a chat model and an embedding model β npm run docker:up doesn't pull models for you, since that's a one-time setup step:
npm run docker:up
docker compose --profile ai exec ollama ollama pull llama3.1
docker compose --profile ai exec ollama ollama pull nomic-embed-textCloud (OpenAI):
- Create an API key at platform.openai.com
Set in .env.local:
AI_MODE=cloud
OPENAI_API_KEY=sk-your_openai_key
OPENAI_MODEL=gpt-4o-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIMENSIONS=1536Note:
EMBEDDING_DIMENSIONSmust match the active embedding model's output size and is only applied when the Qdrant collection (documind_chunks) is first created β it does not resize an existing collection. If you switchAI_MODEafter documents have already been embedded, delete thedocumind_chunkscollection in Qdrant and re-upload documents so vector dimensions stay consistent.
Authentication supports two modes controlled by AUTH_MODE (and its client-visible twin, NEXT_PUBLIC_AUTH_MODE) in .env.local. Unlike the other four toggles, local mode here is a library, not a container β Auth.js (NextAuth v5) with a Credentials provider stores email/password accounts (bcrypt-hashed) in whichever MongoDB is already configured, so npm run docker:up has nothing extra to start for this one.
Local (self-hosted email/password) β recommended for development, no external account needed:
Generate a session secret once:
npx auth secretThis writes AUTH_SECRET into .env.local for you (or copy the printed value in manually).
Set in .env.local:
AUTH_MODE=local
NEXT_PUBLIC_AUTH_MODE=local
AUTH_SECRET=your_generated_secretThen visit /sign-up to create the first account β no email verification or password reset flow in local mode (matching the other local modes' zero-external-service approach). Accounts are stored in the users collection of the active MongoDB instance.
Cloud (Clerk):
- Create a free application at clerk.com
- Copy the publishable and secret keys
Set in .env.local:
AUTH_MODE=cloud
NEXT_PUBLIC_AUTH_MODE=cloud
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key
CLERK_SECRET_KEY=sk_test_your_keyNote:
AUTH_MODEchanges what gets rendered client-side (sign-in/up forms, the header's account menu), so switching it requires a full rebuild/restart of the dev server β not just an env var reload β sinceNEXT_PUBLIC_*values are inlined into the client bundle at build time.
# Development mode
npm run dev
# Production build
npm run build
npm startThe application will be available at http://localhost:3000
documind/
βββ app/ # Next.js App Router
β βββ api/ # API routes
β β βββ chat/ # Q&A endpoint
β β βββ documents/ # Document management
β β βββ graph/ # Graph operations
β β βββ search/ # Search functionality
β β βββ upload/ # File upload
β βββ api/
β β βββ auth/
β β βββ [...nextauth]/ # Auth.js route handler (local mode)
β β βββ register/ # Local-mode sign-up endpoint
β βββ chat/ # Chat interface page
β βββ dashboard/ # Main dashboard
β βββ graph/ # Knowledge graph view
β βββ sign-in/ # Authentication pages (mode-conditional UI)
β βββ sign-up/
β βββ layout.tsx # Root layout
β βββ page.tsx # Landing page
βββ components/ # React components
β βββ auth/ # Local-mode sign-in/sign-up forms
β βββ chat/ # Chat interface components
β βββ documents/ # Document management
β βββ graph/ # Graph visualization
β βββ layout/ # Layout components
β βββ providers/ # AuthProvider (Clerk vs Auth.js SessionProvider)
β βββ ui/ # Reusable UI components
βββ lib/ # Utilities and configurations
β βββ ai/ # AI processing modules
β β βββ chat.ts # Chat functionality
β β βββ embeddings.ts # Vector embeddings
β β βββ entities.ts # Entity extraction
β β βββ pipeline.ts # Processing pipeline
β β βββ processing.ts # Text processing
β βββ api/ # API utilities
β βββ auth/ # Unified auth: server (index.ts) + client (client.tsx) hooks
β βββ db/ # Database connections
β β βββ mongodb.ts # MongoDB client
β β βββ neo4j.ts # Neo4j client
β β βββ qdrant.ts # Qdrant client
β βββ storage/ # File storage
βββ types/ # TypeScript definitions
β βββ next-auth.d.ts # Auth.js session/user type augmentation
βββ scripts/
β βββ docker-services.js # Starts/stops only the services set to "local" mode
βββ auth.config.ts # Edge-safe Auth.js config (used by middleware.ts)
βββ auth.ts # Full Auth.js config (Credentials provider, Node runtime only)
βββ docker-compose.yml # MongoDB/Qdrant/Neo4j/MinIO/Ollama, one profile per service
βββ middleware.ts # Clerk or Auth.js middleware, chosen by AUTH_MODE
βββ next.config.ts # Next.js configuration
End-to-end flow from upload to a fully indexed, graph-linked document. The AI-specific steps (chunking, embeddings, entity extraction, resolution, clustering) are covered in detail in AI Systems Deep Dive above β this is the operational view.
- Upload β verify the user session (local Auth.js or Clerk, depending on
AUTH_MODE), save the file to local MinIO or AWS S3, create a document record in MongoDB, and hand off to background processing - Extraction & chunking β pull text from PDF/DOCX/TXT and split it into overlapping word-windows
- Embedding & vector storage β batch-embed every chunk and upsert into Qdrant, scoped to the uploading user
- Knowledge graph construction β extract entities per chunk, link co-occurrences, resolve entities against the user's existing graph, and store everything in Neo4j with proper indexing
- Status updates β the document's
processingStatusmoves throughpending β processing β completed/failed, polled live by the dashboard, with error messages surfaced on failure
Note that resolving duplicate entities and clustering documents by similarity are collection-wide operations, so they're triggered on demand from the graph page rather than automatically per upload (see stage 3β5 of the knowledge-graph pipeline above).
Covered in full in AI Systems Deep Dive β Retrieval-Augmented Generation (RAG) pipeline above β retrieval is a straight Qdrant vector search scoped to the asking user, with Neo4j powering a separate graph-exploration experience rather than feeding the chat prompt directly.
- Interactive Visualization: Cytoscape.js powered graphs with optimized layouts
- Real hover-driven edge labels: connection types stay hidden until you hover the specific edge β a plain
edge:hoverCSS-style selector doesn't exist in Cytoscape core, so this is implemented with realmouseover/mouseoutlisteners - Advanced Filtering: filter by entity type, node cap, and structural noise (chunk nodes, co-occurrence mesh)
- Entity Relationships: explore connections between people, organizations, locations, and concepts
- Document Mapping: visualize how documents relate through shared entities and topics
- Graph Statistics: real-time metrics showing nodes, edges, and entity distributions
- Local mode: Self-hosted email/password via Auth.js β bcrypt-hashed passwords, JWT sessions, no external account needed
- Cloud mode: Clerk-managed sign-up/sign-in flows
- Session Management: Automatic token handling (JWT locally, Clerk-managed in cloud mode)
- Route Protection: Middleware-based access control (Auth.js or Clerk, depending on
AUTH_MODE)
- User Scoping: Complete isolation of user data
- Query Filtering: Automatic user-based filtering
- Access Control: Document ownership verification
- Encrypted Storage: Secure file storage in MinIO or AWS S3
- API Security: Protected routes with authentication
- Error Handling: Safe error messages without data leakage
# Development server
npm run dev
# Production build
npm run build
# Start production server
npm start
# Linting
npm run lint
# Code formatting
npm run format
# Start only the Docker services set to "local" mode in .env.local
npm run docker:up
# Stop them again
npm run docker:down- TypeScript: Full type safety across the application
- Biome: Modern linting and formatting
- Error Boundaries: Graceful error handling
- Loading States: Comprehensive loading indicators
- Database Services: Ensure all databases are accessible
- Environment Variables: Configure production credentials
- File Storage: Set up AWS S3 bucket (or self-host MinIO with
STORAGE_MODE=local) - AI Provider: Configure OpenAI API access (or self-host Ollama with
AI_MODE=local) - Authentication: Configure Clerk for production (or self-host with
AUTH_MODE=local+ a strongAUTH_SECRETβ note that local mode has no email verification or password reset flow, by design; add those yourself before relying on it in production)
- Vercel: Optimal for Next.js applications
- Netlify: Alternative deployment option
- Railway: Full-stack deployment with databases
- AWS/GCP/Azure: Enterprise-grade hosting
- Environment variables configured
- Database connections tested
- File upload limits set
- Authentication flows verified
- Error monitoring enabled
- Performance optimization applied
If sign-in/sign-up fails or protected routes won't load:
- "Configuration" error page or middleware crash on startup β
AUTH_SECRETis missing or empty. Generate one withnpx auth secretand set it in.env.local, then restart the dev server. - Signed in but immediately redirected to
/sign-inβ usually a mismatch between the cookie the session was issued with and the currentAUTH_SECRET/AUTH_MODE; sign out, clear cookies forlocalhost:3000, and sign in again. - "An account with this email already exists" on sign-up β expected; go to
/sign-ininstead. Local mode has no password reset flow (by design β see the Authentication setup section above), so a forgotten password currently means creating a new account or manually updating theuserscollection in MongoDB. - Switched
AUTH_MODEbut UI didn't change βNEXT_PUBLIC_AUTH_MODEis inlined into the client bundle at build time; a dev-server restart (or rebuild in production) is required, not just an env var edit. next buildfails mentioning the Edge Runtime β something inmiddleware.ts's local-mode branch is reachingauth.tsinstead ofauth.config.ts(e.g. through an accidental import).auth.ts's Credentials provider pulls in the MongoDB driver, which isn't Edge-compatible;middleware.tsmust only ever importauth.config.ts.
If you encounter connection errors with MongoDB:
- Switch to local mode β set
MONGODB_MODE=localin.env.local, then start the container:npm run docker:up
- Verify the container is healthy:
docker ps --filter name=mongodb
- Cloud mode issues β check that
MONGODB_URIis correct, includes the right credentials, and that your IP is on the Atlas access list - After changing
MONGODB_MODE, restart the dev server β the client is initialized once per process
If you encounter ENOTFOUND or fetch failed errors with Qdrant:
- Switch to local mode β set
QDRANT_MODE=localin.env.local, then start the container:npm run docker:up
- Verify the container is healthy:
curl http://localhost:6333/healthz
- Cloud mode issues β check that
QDRANT_URLandQDRANT_API_KEYare correct and the cluster is running at cloud.qdrant.io - After changing
QDRANT_MODE, restart the dev server β the client is initialized once per process
If you encounter connection errors with Neo4j:
- Switch to local mode β set
NEO4J_MODE=localin.env.local, then start the container:npm run docker:up
- Verify the container is healthy β open
http://localhost:7474in your browser (login:neo4j/neo4j) - Password mismatch β if you changed the default password in the browser UI, update
NEO4J_LOCAL_PASSWORDin.env.localto match - Cloud mode issues β verify
NEO4J_URI,NEO4J_USERNAME, andNEO4J_PASSWORDmatch your Neo4j Aura instance - After changing
NEO4J_MODE, restart the dev server
If uploads or downloads fail with connection or credential errors:
- Switch to local mode β set
STORAGE_MODE=localin.env.local, then start the container:npm run docker:up
- Verify the container is healthy β open
http://localhost:9001in your browser (login:minioadmin/minioadmin) - Cloud mode issues β check that
AWS_REGION,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_S3_BUCKET_NAMEare correct and the IAM user has read/write access to the bucket - After changing
STORAGE_MODE, restart the dev server β the client is initialized once per process
If chat, embedding, or entity/topic extraction requests fail:
- Switch to local mode β set
AI_MODE=localin.env.local, start the container, and pull models:npm run docker:up docker compose --profile ai exec ollama ollama pull llama3.1 docker compose --profile ai exec ollama ollama pull nomic-embed-text
- Verify Ollama is healthy:
curl http://localhost:11434/api/tags
- Cloud mode issues β check that
OPENAI_API_KEYis correct and has available quota - Embedding dimension mismatch β if you see Qdrant errors about vector size after switching
AI_MODE, delete thedocumind_chunksQdrant collection and re-upload documents (see the AI Provider setup section above) - After changing
AI_MODE, restart the dev server β the client is initialized once per process
- No Connections Visible: Check that documents have been processed and entities extracted
- Cluttered Graph: Turn off "Show node names" for a cleaner view, or hover individual nodes/edges to reveal names on demand
- Performance Issues: Reduce max nodes limit in the filters panel
- Layout Problems: Use graph controls (fit to view, center, reset zoom) to optimize display
- MongoDB: Ensure connection string is correct and database is accessible
- Neo4j: Verify bolt:// URL and credentials are valid
- File Storage: Check access credentials and bucket permissions (MinIO or AWS S3)
- AI Provider: Check
AI_MODE, Ollama connectivity, orOPENAI_API_KEYvalidity
# Set all modes to local in .env.local
# MONGODB_MODE=local
# QDRANT_MODE=local
# NEO4J_MODE=local
# STORAGE_MODE=local
# AI_MODE=local
# AUTH_MODE=local
# NEXT_PUBLIC_AUTH_MODE=local
# Generate an auth session secret (writes AUTH_SECRET into .env.local)
npx auth secret
# Install dependencies
npm install
# Start only the services set to "local" above (reads .env.local automatically)
# β note there's no container for AUTH_MODE, it's a library, not a service
npm run docker:up
# Pull the local chat and embedding models (one-time setup)
docker compose --profile ai exec ollama ollama pull llama3.1
docker compose --profile ai exec ollama ollama pull nomic-embed-text
# Run development server
npm run devTo switch MongoDB, Qdrant, Neo4j, file storage, the AI provider, or authentication between local and cloud, update the mode variable(s) in .env.local, re-run npm run docker:up (it starts newly-local services and leaves the rest alone β run npm run docker:down first if you want to stop a container you just switched away from), and restart the dev server:
| Variable | local |
cloud |
|---|---|---|
MONGODB_MODE |
Docker Desktop (mongodb://localhost:27017) |
MongoDB Atlas (MONGODB_URI) |
QDRANT_MODE |
Docker Desktop (http://localhost:6333) |
Qdrant Cloud (QDRANT_URL + QDRANT_API_KEY) |
NEO4J_MODE |
Docker Desktop (bolt://localhost:7687) |
Neo4j Aura (NEO4J_URI + credentials) |
STORAGE_MODE |
Docker Desktop MinIO (http://localhost:9000) |
AWS S3 (AWS_S3_BUCKET_NAME + credentials) |
AI_MODE |
Docker Desktop Ollama (http://localhost:11434) |
OpenAI (OPENAI_API_KEY) |
AUTH_MODE (+ NEXT_PUBLIC_AUTH_MODE) |
Self-hosted Auth.js (AUTH_SECRET, no container) |
Clerk (CLERK_SECRET_KEY + publishable key) |
The default for all six is cloud when the variable is unset in code β the shipped .env.example sets each to local for a zero-config dev setup. Switching AI_MODE also changes the embedding model, so EMBEDDING_DIMENSIONS and the Qdrant collection must be kept in sync (see the AI Provider setup section above). AUTH_MODE is the only one of the six needing its NEXT_PUBLIC_ twin kept in sync too, and the only one requiring a full restart rather than a hot env reload, since it changes what renders client-side.
The application includes built-in connection testing and will provide clear error messages for misconfigured services.
- Efficient Indexing: Optimized Qdrant collections
- Batch Processing: Bulk operations for embeddings
- Caching Strategy: Smart result caching
- MongoDB Indexes: Optimized query performance
- Neo4j Optimization: Efficient graph traversal
- Connection Pooling: Managed database connections
- Next.js Optimization: Built-in performance features
- Component Optimization: Memoization and lazy loading
- Bundle Optimization: Efficient code splitting
We welcome contributions! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- TypeScript: Maintain type safety
- Testing: Add tests for new features
- Documentation: Update docs for changes
- Code Style: Follow established patterns
This project is licensed under the MIT License - see the LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Built with β€οΈ by Kanugu Rajesh













