A role-based document Retrieval-Augmented-Generation playground built with .NET 10, React + TypeScript + i18n, MongoDB, Qdrant, OpenAI, and JWT — laid out with Clean Architecture + DDD.
It implements the spec in DOCX.md:
- Login (no public registration)
- Per-document role-based read access (rejects with the required role list when denied)
- Upload with mandatory type + allowed roles, including 3 built-in types: tài chính, chính sách bảo mật, khác
- Create new custom types
- Re-uploading a document of the same name & type re-chunks, re-embeds, and bumps to a new version
- Chat endpoint that retrieves only chunks from documents the caller is allowed to read
RagSystem.sln
├── Directory.Build.props ← target framework + central PM
├── Directory.Packages.props ← central package versions
├── docker-compose.yml ← MongoDB + Qdrant
├── src/
│ ├── RagSystem.Domain/ ← entities (User, Document, DocumentType), invariants
│ ├── RagSystem.Application/ ← use cases, DTOs, port interfaces (Clean Arch boundary)
│ ├── RagSystem.Infrastructure/ ← Mongo, Qdrant, OpenAI, JWT, BCrypt, file extractors, seeder
│ └── RagSystem.Api/ ← ASP.NET Core controllers, DI, JWT auth, Swagger, CORS
└── client/ ← React + Vite + TypeScript + i18next (EN / VI)
- User:
Id,Email,PasswordHash,Roles[]— multiple roles allowed. - Document:
Id,FileName,Type,Version,UploadedAt,Status,AllowedRoles[],UploadedBy,SizeBytes,ContentHash.CanBeReadBy(userRoles)is the single guard for read access.Versionis monotonic per(FileName, Type).
- DocumentType: name + display name;
IsBuiltIndistinguishes seeded vs. user-created.
Qdrant collection rag_documents (cosine, 1536-dim by default to match text-embedding-3-small). Each chunk is stored with payload {document_id, document_type, version, chunk_index, text, allowed_roles}. Search is filtered server-side: at least one of the caller's roles must overlap with allowed_roles, so unauthorized chunks never reach the LLM.
- .NET 10 SDK
- Node.js 20+
- Docker (for MongoDB + Qdrant)
- An OpenAI API key
You can run the whole stack in Docker or run the API / client locally against dockerised Mongo + Qdrant.
cp .env.example .env # then edit .env to add your OPENAI_API_KEY
docker compose up -d --buildServices that come up:
| Service | Container | Host URL |
|---|---|---|
| Mongo | rag-mongo |
localhost:27017 |
| Qdrant | rag-qdrant |
localhost:6333 / :6334 |
| API | rag-api |
http://localhost:5080 |
| Client | rag-client |
http://localhost:5173 |
The client container runs nginx and proxies /api/* to the API container over the compose network. API picks up config from env vars (Mongo__*, Qdrant__*, OpenAi__*, Jwt__*).
Tail logs:
docker compose logs -f apiTear down (keeps volumes):
docker compose downWipe data too:
docker compose down -vStart just the infra:
docker compose up -d mongo qdrantStore your OpenAI key in .NET user secrets (not in appsettings.json):
dotnet user-secrets set "OpenAi:ApiKey" "sk-..." --project src/RagSystem.ApiThen run:
dotnet run --project src/RagSystem.ApiAPI listens on http://localhost:5080, OpenAPI at /openapi/v1.json, UI at /scalar/v1. On first start the seeder creates the two test accounts and the three built-in document types.
React client:
cd client
npm install
npm run devThe Vite dev server runs on http://localhost:5173 and proxies /api/* to the API.
| Password | Roles | |
|---|---|---|
admin@gmail.com |
12345678Aa |
admin, user |
user@gmail.com |
12345678Aa |
user |
Try the role-based access flow:
- Sign in as
admin@gmail.com, upload a document with allowed roles =adminonly. - Sign out, sign in as
user@gmail.com— the document will not appear in the list, and asking about it via chat returns no context. - Re-upload the same file with the same type as
admin@gmail.com— version is bumped to v2, the old chunks remain in Qdrant under v1, new chunks land under v2.
| Method | Path | Auth | Notes |
|---|---|---|---|
| POST | /api/auth/login |
No | { email, password } → { token, email, roles } |
| GET | /api/me |
JWT | Echo current claims |
| GET | /api/document-types |
JWT | List built-in + custom types |
| POST | /api/document-types |
JWT | { name, displayName } create new type |
| GET | /api/documents |
JWT | List documents the caller's roles can read |
| GET | /api/documents/{id} |
JWT | 403 with requiredRoles if caller lacks access |
| POST | /api/documents/upload |
JWT | multipart: file, type, allowedRoles (csv) |
| POST | /api/chat/ask |
JWT | { question, topK } → answer + cited sources |
Supported upload formats: .pdf, .docx, .txt, .md.
- Backend — .NET 10 (
net10.0), ASP.NET Core, MongoDB.Driver, Qdrant.Client (gRPC), OpenAI .NET SDK, BCrypt, JWT bearer, central package management viaDirectory.Packages.props - Frontend — React 18, TypeScript, Vite, react-router, i18next + browser language detector (English & Vietnamese)
- Infrastructure — MongoDB 7, Qdrant v1.12, Docker Compose
MongoConnectionException— confirmdocker compose psshowsrag-mongois up; the API will keep retrying on subsequent calls.The model produced invalid content/ 401 from OpenAI — set a validOpenAi:ApiKey. You can also set the embedding/chat model names inappsettings.json.Vector dimension mismatchfrom Qdrant — drop therag_documentscollection in Qdrant if you switch embedding models with different dimensions.- CORS errors in the browser — the API allows
http://localhost:5173andhttp://localhost:3000by default; add your origin inProgram.cs.