An industry-oriented monorepo for a multi-tenant AI customer support assistant for SaaS companies. The system answers customer questions from company knowledge sources such as documentation, FAQs, PDFs, and historical tickets. When confidence is low, it escalates to a human support team by creating a ticket.
This repository intentionally starts with architecture, service boundaries, deployment wiring, and runnable application foundations. Detailed RAG business logic, ingestion workers, ranking strategies, billing, and admin workflows should be implemented incrementally on top of this base.
- Multi-tenant SaaS support architecture
- JWT-based authentication with owner organization registration
- Retrieval-augmented generation over organization-specific knowledge bases
- Document ingestion foundation for docs, FAQs, PDFs, and past tickets
- Confidence-based human escalation path
- PostgreSQL for relational product data
- Redis for cache, rate limiting, queues, and worker coordination
- Qdrant for vector search
- OpenAI-compatible AI provider configuration
- Docker Compose stack for local development
- React, TypeScript, and Vite frontend shell
- FastAPI backend with versioned API routing
| Layer | Technology |
|---|---|
| Frontend | React, TypeScript, Vite |
| Backend | FastAPI, Python |
| Database | PostgreSQL, SQLAlchemy, Alembic |
| Cache and Queue | Redis |
| Vector Database | Qdrant |
| AI Provider | OpenAI-compatible API |
| Auth | JWT, bcrypt password hashing |
| Deployment | Docker, Docker Compose |
.
|-- apps
| |-- backend
| | |-- app
| | | |-- api
| | | |-- core
| | | |-- db
| | | |-- models
| | | |-- repositories
| | | |-- schemas
| | | |-- services
| | | |-- tenants
| | | |-- utils
| | | `-- workers
| | |-- scripts
| | |-- tests
| | |-- Dockerfile
| | |-- alembic.ini
| | `-- requirements.txt
| `-- frontend
| |-- src
| |-- Dockerfile
| `-- package.json
|-- docs
| `-- architecture.md
|-- infra
| `-- docker-compose.yml
|-- .env.example
`-- README.md
- Docker and Docker Compose
- Node.js 20+ for frontend development outside Docker
- Python 3.12+ for backend development outside Docker
- Copy the environment template:
cp .env.example .env-
Update
.envwith local secrets and provider keys. -
Start the stack:
docker compose -f infra/docker-compose.yml up --build- Open the services:
- Frontend:
http://localhost:5173 - Backend API:
http://localhost:8000 - Backend health check:
http://localhost:8000/health - Backend API docs:
http://localhost:8000/api/v1/docs - Qdrant dashboard/API:
http://localhost:6333
The complete local template lives in .env.example. Key groups:
- App metadata:
APP_ENV,APP_NAME,API_V1_PREFIX - Frontend API config:
VITE_API_BASE_URL - Database:
POSTGRES_*,DATABASE_URL,DB_POOL_SIZE,DB_MAX_OVERFLOW - Redis:
REDIS_URL - Qdrant:
QDRANT_URL,QDRANT_COLLECTION_PREFIX - AI provider:
OPENAI_API_KEY,OPENAI_BASE_URL,OPENAI_MODEL,OPENAI_EMBEDDING_MODEL,EMBEDDING_VECTOR_SIZE,EMBEDDING_MAX_RETRIES - Auth:
JWT_SECRET_KEY,JWT_ALGORITHM,ACCESS_TOKEN_EXPIRE_MINUTES - RAG behavior:
RAG_TOP_K,RAG_MIN_CONFIDENCE,DOCUMENT_CHUNK_SIZE,DOCUMENT_CHUNK_OVERLAP,MAX_DOCUMENT_UPLOAD_SIZE_BYTES - Organization controls:
DEFAULT_ORGANIZATION_SLUG,ORGANIZATION_HEADER_NAME - Seed data:
DEMO_ORGANIZATION_*,DEMO_ADMIN_*
Do not commit real secrets. Use a secret manager or deployment platform variables outside local development.
cd apps/backend
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
alembic upgrade head
python scripts/seed_demo.py
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Expected backend layers:
api: HTTP routers, dependencies, request validation, auth guardscore: settings, security, logging, application configurationdb: PostgreSQL sessions and Alembic migrationsmodels: SQLAlchemy persistence modelsschemas: API and internal DTOsrepositories: tenant-scoped data access helpersservices: RAG, ticketing, ingestion, AI provider orchestrationtenants: organization resolution and isolation policiesworkers: background jobs for ingestion, embeddings, and ticket sync
Run backend tests:
cd apps/backend
pytestRegister creates an organization and an owner user:
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"organization_name": "Acme Support",
"organization_slug": "acme-support",
"email": "owner@acme.example",
"password": "StrongPass123!",
"full_name": "Acme Owner"
}'Login returns a JWT, user details, and organization details:
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"owner@acme.example","password":"StrongPass123!"}'Use the returned JWT from the frontend as a bearer token on authenticated requests:
Authorization: Bearer <access_token>Fetch the authenticated user:
curl http://localhost:8000/api/v1/auth/me \
-H "Authorization: Bearer <access_token>"Owners and admins can create, update, and delete knowledge documents. Owners, admins, and agents can list and read documents. Customers cannot access the document management API.
Create a document from raw text:
curl -X POST http://localhost:8000/api/v1/knowledge/documents \
-H "Authorization: Bearer <access_token>" \
-F "title=Billing FAQ" \
-F "source_type=text" \
-F "content_text=Billing questions and answers live here."Upload a text, Markdown, or PDF file:
curl -X POST http://localhost:8000/api/v1/knowledge/documents \
-H "Authorization: Bearer <access_token>" \
-F "title=Support Runbook" \
-F "source_type=pdf" \
-F "file=@./support-runbook.pdf;type=application/pdf"List documents with pagination:
curl "http://localhost:8000/api/v1/knowledge/documents?limit=20&offset=0" \
-H "Authorization: Bearer <access_token>"Update a document:
curl -X PATCH http://localhost:8000/api/v1/knowledge/documents/<document_id> \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"title":"Updated Billing FAQ","content_text":"Updated content."}'Delete a document:
curl -X DELETE http://localhost:8000/api/v1/knowledge/documents/<document_id> \
-H "Authorization: Bearer <access_token>"Document status starts as uploaded. Use the indexing endpoint to move documents through processing, indexed, or failed.
cd apps/frontend
npm install
npm run devExpected frontend layers:
- API client and auth state
- Organization-aware routing
- Support chat widget
- Admin knowledge-base management
- Ticket escalation dashboard
- Observability and feedback views
Add these as implementation grows:
- Backend:
pytest,ruff,mypy - Frontend:
vitest,eslint,prettier,tsc --noEmit - Infrastructure: compose validation and container health checks
- Security: dependency scanning and secret scanning
- Add background ingestion workers for PDF, FAQ, website, and ticket imports.
- Add support ticket assignment notifications and external helpdesk adapters.
- Build admin UI for knowledge-base uploads, ticket review, and analytics.
- Add observability dashboards for confidence, deflection, and escalation quality.
Index a document after upload:
curl -X POST http://localhost:8000/api/v1/knowledge/documents/<document_id>/index \
-H "Authorization: Bearer <access_token>"List persisted chunks:
curl http://localhost:8000/api/v1/knowledge/documents/<document_id>/chunks \
-H "Authorization: Bearer <access_token>"Indexing sets the document to processing, writes overlapping chunks to PostgreSQL, generates embeddings with the configured OpenAI-compatible provider, upserts vectors to Qdrant, and then marks the document indexed. If processing fails, the document is marked failed. Re-indexing deletes old chunks and old vectors for the same organization/document before writing the new index.
Qdrant collection: organization_knowledge by default. Each point payload includes organization_id, document_id, chunk_id, title, and source_type.
Create a conversation:
curl -X POST http://localhost:8000/api/v1/chat/conversations \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"title":"Password help"}'Ask a question:
curl -X POST http://localhost:8000/api/v1/chat/conversations/<conversation_id>/messages \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"message":"How do I reset my password?"}'Example response:
{
"assistant_message": {
"id": "...",
"sender_type": "assistant",
"content": "You can reset your password from Account Settings.",
"confidence_score": 0.795,
"sources": [
{
"document_id": "...",
"document_title": "Password Reset FAQ",
"chunk_id": "...",
"similarity_score": 0.91
}
]
},
"confidence_score": 0.795,
"sources": [
{
"document_id": "...",
"document_title": "Password Reset FAQ",
"chunk_id": "...",
"similarity_score": 0.91
}
],
"should_escalate": false
}Confidence combines the top retrieved chunk similarity scores, the number of relevant chunks, and whether the answer says it does not know. If confidence is below RAG_MIN_CONFIDENCE, should_escalate is true and the conversation is marked escalated.
Create a support ticket manually from a conversation:
curl -X POST http://localhost:8000/api/v1/tickets \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "<conversation_id>",
"priority": "high"
}'Customers can explicitly request a human for their own conversation:
curl -X POST http://localhost:8000/api/v1/chat/conversations/<conversation_id>/escalate \
-H "Authorization: Bearer <customer_access_token>"Agents, admins, and owners can list and inspect organization tickets:
curl "http://localhost:8000/api/v1/tickets?status=open&priority=urgent&assigned_agent_id=<agent_id>" \
-H "Authorization: Bearer <access_token>"Update ticket status, priority, or assignment:
curl -X PATCH http://localhost:8000/api/v1/tickets/<ticket_id> \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"status":"assigned","assigned_agent_id":"<agent_id>"}'Manual escalation always creates a ticket and marks the conversation escalated. Low-confidence chat answers return should_escalate: true; they create a ticket only when AUTO_CREATE_TICKET_ON_LOW_CONFIDENCE=true. Generated ticket descriptions include the conversation title, latest customer message, assistant answer, confidence score, and source references.