This guide helps developers and AI agents understand how to work on this codebase effectively.
- Codebase Overview
- Architecture Patterns
- Development Workflow
- Adding New Features
- Code Style & Conventions
- Testing
- Common Tasks
- For AI Agents
.
├── backend/ # Python FastAPI backend
│ ├── app/
│ │ ├── api/v1/ # API endpoints (REST routes)
│ │ ├── core/ # Core functionality (config, security, logging)
│ │ ├── db/ # Database models and session management
│ │ ├── services/ # Business logic layer
│ │ ├── tasks/ # Celery async tasks
│ │ └── schemas/ # Pydantic models for validation
│ ├── alembic/ # Database migrations
│ ├── requirements.txt # Python dependencies
│ └── Dockerfile # Backend container definition
├── frontend/ # React frontend
│ ├── src/
│ │ ├── pages/ # Page components
│ │ ├── components/ # Reusable components
│ │ └── contexts/ # React context providers
│ └── package.json # Node dependencies
├── monitoring/ # Prometheus & Grafana configs
├── storage/ # FAISS indices and uploaded files
└── docker-compose.yml # Local development orchestration
Backend:
- FastAPI - Web framework
- SQLAlchemy - ORM
- Alembic - Migrations
- Pydantic - Data validation
- Celery - Async tasks
- FAISS - Vector search
Frontend:
- React 18 - UI framework
- React Router - Routing
- Tailwind CSS - Styling
- Axios - HTTP client
- Vite - Build tool
Rule: API endpoints should be thin wrappers that delegate to service functions.
# Good: API endpoint delegates to service
@router.post("/upload")
async def upload_document(file: UploadFile, db: Session = Depends(get_db)):
return ingestion_service.upload_document(db, file)
# Bad: Business logic in API endpoint
@router.post("/upload")
async def upload_document(file: UploadFile, db: Session = Depends(get_db)):
# Don't put business logic here
content = await file.read()
# ... processing logic ...Why: Separates concerns, makes code testable, allows reuse.
Rule: Use FastAPI Depends() for database sessions, authentication, and tenant extraction.
# Standard pattern for protected endpoints
@router.get("/items")
async def get_items(
tenant_id: int = Depends(get_current_tenant),
db: Session = Depends(get_db)
):
# tenant_id and db are automatically injected
return service.get_items(db, tenant_id)Common Dependencies:
get_db- Database sessionget_current_user- Authenticated userget_current_tenant- Tenant ID from JWTrequire_admin_role- Admin-only access
Rule: Always filter by tenant_id in database queries.
# Good: Tenant-scoped query
def get_documents(db: Session, tenant_id: int):
return db.query(Document).filter(Document.tenant_id == tenant_id).all()
# Bad: Missing tenant filter
def get_documents(db: Session):
return db.query(Document).all() # Security risk!Why: Ensures complete data isolation between tenants.
# Backend
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Frontend
cd frontend
npm install
# Start infrastructure
docker-compose up -d postgres rediscd backend
alembic upgrade head# Terminal 1: Backend API
cd backend
uvicorn app.main:app --reload
# Terminal 2: Frontend
cd frontend
npm run dev
# Terminal 3: Celery Worker (optional)
cd backend
celery -A celery_worker worker --loglevel=info- Backend changes auto-reload (--reload flag)
- Frontend changes hot-reload (Vite HMR)
- Database changes require new migration
cd backend
alembic revision --autogenerate -m "description of changes"
alembic upgrade headStep 1: Create Pydantic schema in app/schemas/
# app/schemas/example.py
from pydantic import BaseModel
class ExampleCreate(BaseModel):
name: str
description: str
class ExampleResponse(BaseModel):
id: int
name: str
class Config:
from_attributes = TrueStep 2: Create service function in app/services/
# app/services/example_service.py
from sqlalchemy.orm import Session
from app.db.models.example import Example
from app.schemas.example import ExampleCreate
def create_example(db: Session, tenant_id: int, data: ExampleCreate):
example = Example(
tenant_id=tenant_id,
name=data.name,
description=data.description
)
db.add(example)
db.commit()
db.refresh(example)
return exampleStep 3: Create API endpoint in app/api/v1/
# app/api/v1/example.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.dependencies import get_current_tenant
from app.schemas.example import ExampleCreate, ExampleResponse
from app.services import example_service
router = APIRouter()
@router.post("/", response_model=ExampleResponse)
async def create_example(
data: ExampleCreate,
tenant_id: int = Depends(get_current_tenant),
db: Session = Depends(get_db)
):
return example_service.create_example(db, tenant_id, data)Step 4: Register router in app/main.py
from app.api.v1 import example
app.include_router(example.router, prefix="/api/v1/example", tags=["example"])Step 1: Create page component in frontend/src/pages/
// frontend/src/pages/Example.jsx
import { useState } from 'react'
import axios from 'axios'
export default function Example() {
const [data, setData] = useState(null)
const handleSubmit = async () => {
const response = await axios.post('/api/v1/example', {
name: 'Test',
description: 'Test description'
})
setData(response.data)
}
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Example Page</h1>
<button onClick={handleSubmit}>Submit</button>
</div>
)
}Step 2: Add route in frontend/src/App.jsx
import Example from './pages/Example'
<Route path="example" element={<Example />} />Step 3: Add navigation link in frontend/src/components/Layout.jsx
{ path: '/example', label: 'Example' }Step 1: Create model in app/db/models/
# app/db/models/example.py
from sqlalchemy import Column, Integer, String, ForeignKey
from app.db.base import Base
class Example(Base):
__tablename__ = "examples"
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=False, index=True)
name = Column(String, nullable=False)
description = Column(String)Step 2: Import in app/db/models/__init__.py
from .example import ExampleStep 3: Create migration
alembic revision --autogenerate -m "add example table"
alembic upgrade headImportant: Always include tenant_id for multi-tenancy!
- Type Hints: Use type hints for function parameters and returns
- Docstrings: Add docstrings for public functions (if needed)
- Naming:
- Functions:
snake_case - Classes:
PascalCase - Constants:
UPPER_SNAKE_CASE
- Functions:
- Imports: Group imports (stdlib, third-party, local)
# Good
from typing import List, Optional
from sqlalchemy.orm import Session
from app.db.models import User
def get_users(db: Session, tenant_id: int) -> List[User]:
"""Get all users for a tenant."""
return db.query(User).filter(User.tenant_id == tenant_id).all()- Components: PascalCase for component names
- Functions: camelCase for functions
- Hooks: Prefix with
use(e.g.,useAuth) - Props: Destructure props in function signature
// Good
export default function UserProfile({ userId, name }) {
const { user, loading } = useAuth()
if (loading) return <div>Loading...</div>
return <div>{name}</div>
}- Python:
snake_case.py - React Components:
PascalCase.jsx - Utilities:
snake_case.jsorcamelCase.js
# Backend tests
cd backend
pytest
# Frontend tests (if configured)
cd frontend
npm testBackend Test Example:
# tests/test_example.py
import pytest
from app.services import example_service
from app.db.session import SessionLocal
def test_create_example():
db = SessionLocal()
try:
result = example_service.create_example(
db,
tenant_id=1,
data=ExampleCreate(name="Test", description="Test")
)
assert result.name == "Test"
finally:
db.close()- Add column to model in
app/db/models/ - Create migration:
alembic revision --autogenerate -m "add field" - Update Pydantic schema in
app/schemas/ - Update service functions if needed
- Run migration:
alembic upgrade head
from app.core.dependencies import get_current_user, get_current_tenant
@router.get("/protected")
async def protected_endpoint(
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_current_tenant)
):
# Endpoint is now protected
return {"user_id": current_user.id, "tenant_id": tenant_id}from app.core.dependencies import require_admin_role
@router.delete("/admin-only", dependencies=[Depends(require_admin_role)])
async def admin_endpoint():
# Only admins can access
return {"message": "Admin access"}- Create task function in
app/tasks/
# app/tasks/example_tasks.py
from app.tasks.celery_app import celery_app
@celery_app.task
def process_example(example_id: int):
# Async processing
pass- Call from service or API endpoint
from app.tasks.example_tasks import process_example
process_example.delay(example_id)-
Understand the Architecture First
- Read
docs/ARCHITECTURE.mdfor system design - Understand service layer pattern
- Know multi-tenancy enforcement
- Read
-
Follow Existing Patterns
- Look at similar features for reference
- Match code style and structure
- Use existing dependencies and utilities
-
Respect Multi-Tenancy
- Always include
tenant_idin queries - Use
get_current_tenantdependency - Never expose cross-tenant data
- Always include
-
Service Layer First
- Implement business logic in services
- Keep API endpoints thin
- Make services testable
-
Update Documentation
- Update relevant docs when adding features
- Add examples to this guide if creating new patterns
- Keep README.md current
Authentication Pattern:
current_user: User = Depends(get_current_user)
tenant_id: int = Depends(get_current_tenant)Service Call Pattern:
result = service_function(db, tenant_id, data)Error Handling Pattern:
try:
result = service_function(...)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))Database Query Pattern:
db.query(Model).filter(Model.tenant_id == tenant_id).filter(...).all()- Check Dependencies: Ensure tenant_id is included
- Check Permissions: Verify RBAC if needed
- Check Migrations: Create migration for schema changes
- Check Tests: Add/update tests for new features
- Check Docs: Update relevant documentation
- Use semantic search for understanding features
- Use grep for finding specific patterns
- Check
app/main.pyfor router registration - Check
app/db/models/__init__.pyfor model imports - Check
alembic/versions/for migration history
- Check
DATABASE_URLin.env - Verify PostgreSQL is running:
docker ps - Check migration status:
alembic current
- Ensure virtual environment is activated
- Check
__init__.pyfiles exist - Verify Python path includes
backend/
- Clear
node_modulesand reinstall:rm -rf node_modules && npm install - Check Node version:
node --version(should be 18+) - Clear Vite cache:
rm -rf node_modules/.vite
- Check current migration:
alembic current - Review migration files in
alembic/versions/ - Consider manual migration if auto-generate fails
- Check existing code for similar patterns
- Review
docs/ARCHITECTURE.mdfor design decisions - Check API docs at
/docsendpoint - Review git history for context on changes