A production-style Retrieval-Augmented Generation (RAG) backend that lets authenticated users upload PDFs and chat with them. Answers are grounded in the documents and returned with source citations (file + page). Each chat keeps its own memory and its own private set of documents.
Frontend repo: RAG-Frontend — Next.js chat UI that consumes this API.
- 🔐 Authentication — email/password with secure httpOnly cookie sessions (Better Auth)
- 💬 Multiple chats per user — create, list, and resume past conversations (ChatGPT-style)
- 🧠 Per-chat memory — conversation history persisted in MongoDB, fed back to the model
- 📄 PDF ingestion — async, queue-based processing so uploads never block the API
- 🔎 Grounded answers with citations — responses cite the source file and page
- 🛡️ Per-chat document isolation — a chat can only retrieve its own documents (multi-tenant RAG)
- ⚡ Background worker — heavy embedding/indexing runs outside the request cycle
| Layer | Technology |
|---|---|
| Runtime / Framework | Node.js, Express 5 |
| LLM | Google Gemini 2.5 Flash (via @langchain/google-genai) |
| Embeddings | Google gemini-embedding-001 |
| Vector DB | Qdrant |
| Job Queue | BullMQ + Valkey/Redis |
| Database | MongoDB (Mongoose for app data) |
| Auth | Better Auth (MongoDB adapter) |
| PDF parsing | pdf-parse via LangChain PDFLoader |
| Orchestration | LangChain |
┌──────────────────────────────────────────┐
│ Express API │
Upload PDF ─────────► │ POST /upload/pdf ──┐ │
│ │ enqueue job │
│ ▼ │
│ BullMQ (Valkey/Redis) │
└──────────────────────┬───────────────────-┘
│ job
▼
┌──────────────────────────────────────────┐
│ Worker │
│ parse PDF → chunk → embed (Gemini) │
│ tag chunks { userId, chatId, fileName } │
│ store vectors ──────────────► Qdrant │
└────────────────────────────────────────────┘
Ask question ────────► POST /chat
│ 1. verify chat belongs to user (MongoDB)
│ 2. embed question
│ 3. similarity search in Qdrant
│ FILTER: metadata.chatId == this chat
│ 4. build prompt (context + chat history)
│ 5. Gemini 2.5 Flash → grounded answer
│ 6. persist messages (MongoDB)
└─► { message, citations:[{source, page}] }
All chunks live in one Qdrant collection (rag-gemini). Each chunk is tagged with its chatId at indexing time. At query time we run a filtered similarity search so only the current chat's chunks are candidates — other chats' documents are never even considered. Ownership is also enforced first (Chat.findOne({ _id, userId })), so users can only ever query their own chats.
- Node.js 18+
- Docker (for Qdrant, Valkey, MongoDB)
- A Google Gemini API key → Google AI Studio
docker compose up -d # starts qdrant (:6333), valkey (:6379), mongo (:27017)npm install --legacy-peer-deps
--legacy-peer-depsis required due to a peer-dependency conflict in@langchain/community.
Copy .env.example → .env and fill in:
QDRANT_URL=http://localhost:6333
GOOGLE_API_KEY=your_gemini_api_key
MONGODB_URI=mongodb://localhost:27017/rag
BETTER_AUTH_SECRET=<a long random string>
BETTER_AUTH_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000Generate a secret:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"npm run dev # API server on :8000
npm run dev:worker # background PDF processor| Method | Endpoint | Description |
|---|---|---|
POST |
/api/auth/sign-up/email |
Register { name, email, password } |
POST |
/api/auth/sign-in/email |
Login { email, password } |
POST |
/api/auth/sign-out |
Logout |
GET |
/api/auth/get-session |
Current session |
| Method | Endpoint | Description |
|---|---|---|
POST |
/chats |
Create a new chat |
GET |
/chats |
List the user's chats (newest first) |
GET |
/chats/:id/messages |
Get a chat's message history |
POST |
/upload/pdf |
Upload a PDF to a chat — form-data: pdf (file), chatId |
POST |
/chat |
Ask a question — { chatId, userQuery } → { message, citations } |
RAG-Backend/
├── index.js # Express app, routes, RAG /chat endpoint
├── worker.js # BullMQ worker: parse → embed → store
├── lib/
│ ├── auth.js # Better Auth config (MongoDB, cookies)
│ └── db.js # Mongoose connection
├── models/
│ ├── Chat.js # Chat (conversation) schema
│ └── Message.js # Message schema (per-chat memory)
├── middleware/
│ └── requireAuth.js # Session validation → req.userId
├── utils/
│ └── qdrant.js # Embeddings + Qdrant vector store
├── docker-compose.yml # qdrant + valkey + mongo
└── .env.example
Better Auth manages four MongoDB collections:
user— profile (name, email)account— credentials (hashed password / linked OAuth providers)session— active login sessionsverification— email-verify / reset tokens
Sessions use httpOnly cookies — the browser sends them automatically, and the frontend never stores tokens manually. requireAuth middleware validates the cookie on protected routes and attaches req.userId.
- Chat history is capped to the last 10 messages per request to control token usage (Gemini free-tier friendly).
- Retrieval uses
k=4chunks — tunable for recall vs. cost. - Roadmap: streaming responses, Google OAuth, document management (delete/list per chat), rate limiting.
ISC