Skip to main content

ADR-003: FastAPI (Standalone) + Django REST Framework (CODITECT Integration)

Status: Proposed Date: 2025-11-26 Deciders: Architecture Team, CODITECT Integration Team Related ADRs: ADR-001 (Hybrid Architecture), ADR-002 (PostgreSQL + Weaviate), ADR-004 (Multi-Tenant RLS)


Context

The context intelligence platform must operate in two deployment modes (per ADR-001: Hybrid Architecture):

  1. Standalone Mode: Independent SaaS product for any AI coding assistant user
  2. CODITECT Mode: Integrated Django app within existing CODITECT platform

Each mode has different requirements:

Standalone Mode Requirements

  • Fast, async API for real-time conversation capture
  • Independent authentication (JWT, OAuth2)
  • Independent database (own PostgreSQL instance)
  • Modern API documentation (OpenAPI/Swagger)
  • Lightweight deployment (Docker, Kubernetes)
  • Target market: GitHub Copilot, Claude Code, Cursor users ($26B market)

CODITECT Integration Requirements

  • Reuse existing Django infrastructure (60% code reuse target)
  • Shared authentication (Django session auth)
  • Shared database (CODITECT's PostgreSQL with organization_id FK)
  • Shared license management (feature flags per tier)
  • Seamless UI integration (same look and feel)
  • Target market: CODITECT ecosystem (upsell/retention)

Current CODITECT Stack

Backend: Django 4.2 + Django REST Framework
Database: PostgreSQL 14 (upgrading to 15)
Auth: Django authentication + django-multitenant
License: Custom Django app (Starter/Pro/Enterprise tiers)
Frontend: React 18 + TypeScript
Deployment: GCP Cloud Run + Cloud Build

Decision Drivers

  1. Code Reuse: Maximize shared codebase (85% target from ADR-001)
  2. Performance: <100ms p95 API latency for both modes
  3. Development Speed: Time-to-market for standalone mode
  4. Maintenance: Single team maintains both modes
  5. Integration Complexity: Easy Django integration without forking
  6. Future Flexibility: Can pivot to standalone-only or CODITECT-only

Decision

We will implement a pluggable dual-framework architecture:

  1. Core Layer (85%): Framework-agnostic business logic

    • Python data classes for models
    • Async services (PostgreSQL, Weaviate, Celery)
    • Shared logic (search, analytics, correlation)
  2. Standalone Mode (7.5%): FastAPI

    • API endpoints using FastAPI
    • JWT authentication (PyJWT)
    • Independent deployment
    • SQLAlchemy ORM (async)
  3. CODITECT Mode (7.5%): Django REST Framework

    • Django models extending TenantModel
    • Django authentication
    • Django admin integration
    • Django ORM with django-multitenant

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│ Core Layer (85% - Framework Agnostic) │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Business Logic (Python Dataclasses) │ │
│ │ - Conversation, Message, Commit, User, Org │ │
│ │ - Validation, serialization, business rules │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Services (Async Python) │ │
│ │ - SearchService (PostgreSQL + Weaviate) │ │
│ │ - AnalyticsService (aggregations) │ │
│ │ - CorrelationService (conversation ↔ commit) │ │
│ │ - GitSyncService (webhook ingestion) │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Repository Layer (Abstract Interface) │ │
│ │ - ConversationRepo, CommitRepo, UserRepo │ │
│ │ - Abstract methods (get, create, update, delete) │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

┌───────────────────┴───────────────────┐
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ Standalone Mode │ │ CODITECT Mode │
│ (FastAPI - 7.5%) │ │ (Django - 7.5%) │
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ API Layer │ │ │ │ API Layer │ │
│ │ (FastAPI) │ │ │ │ (Django REST)│ │
│ │ - Endpoints │ │ │ │ - ViewSets │ │
│ │ - Pydantic │ │ │ │ - Serializers│ │
│ └──────────────┘ │ │ └──────────────┘ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ Auth │ │ │ │ Auth │ │
│ │ (JWT/OAuth2) │ │ │ │ (Django) │ │
│ └──────────────┘ │ │ └──────────────┘ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ ORM │ │ │ │ ORM │ │
│ │ (SQLAlchemy) │ │ │ │ (Django ORM) │ │
│ └──────────────┘ │ │ └──────────────┘ │
└──────────────────┘ └──────────────────┘
↓ ↓
PostgreSQL CODITECT PostgreSQL
(dedicated) (shared, org_id FK)

Implementation

Core Layer: Framework-Agnostic Business Logic

# core/models/conversation.py
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
import uuid

@dataclass
class Conversation:
"""Framework-agnostic conversation model"""

id: uuid.UUID
organization_id: uuid.UUID
user_id: uuid.UUID
title: str
created_at: datetime
updated_at: datetime

def __post_init__(self):
"""Validation"""
if not self.title or len(self.title) > 500:
raise ValueError("Title must be 1-500 characters")

# core/services/search_service.py
from typing import List
import asyncio

class SearchService:
"""Framework-agnostic search service"""

def __init__(
self,
conversation_repo: ConversationRepository,
weaviate_client: WeaviateClient
):
self.conversation_repo = conversation_repo
self.weaviate_client = weaviate_client

async def hybrid_search(
self,
organization_id: uuid.UUID,
query: str,
limit: int = 20
) -> List[Conversation]:
"""Hybrid search: PostgreSQL keyword + Weaviate semantic"""

# Run both searches in parallel
keyword_task = self.conversation_repo.keyword_search(
organization_id=organization_id,
query=query,
limit=limit
)

semantic_task = self.weaviate_client.semantic_search(
organization_id=organization_id,
query=query,
limit=limit
)

keyword_results, semantic_results = await asyncio.gather(
keyword_task,
semantic_task
)

# Reciprocal Rank Fusion (RRF)
return self._rrf_fusion(keyword_results, semantic_results)

# core/repositories/base.py
from abc import ABC, abstractmethod
from typing import List, Optional
import uuid

class ConversationRepository(ABC):
"""Abstract repository interface"""

@abstractmethod
async def get(self, id: uuid.UUID) -> Optional[Conversation]:
pass

@abstractmethod
async def create(self, conversation: Conversation) -> Conversation:
pass

@abstractmethod
async def keyword_search(
self,
organization_id: uuid.UUID,
query: str,
limit: int
) -> List[Conversation]:
pass

Standalone Mode: FastAPI Implementation

# standalone/api/routes/conversations.py
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List
import uuid

from core.services.search_service import SearchService
from core.models.conversation import Conversation
from standalone.auth.jwt import get_current_user
from standalone.dependencies import get_search_service

router = APIRouter()

class ConversationResponse(BaseModel):
"""Pydantic response model"""
id: str
organization_id: str
user_id: str
title: str
created_at: str

@router.get("/conversations/search", response_model=List[ConversationResponse])
async def search_conversations(
q: str,
limit: int = 20,
current_user = Depends(get_current_user),
search_service: SearchService = Depends(get_search_service)
):
"""Hybrid search endpoint"""

results = await search_service.hybrid_search(
organization_id=current_user.organization_id,
query=q,
limit=limit
)

return [
ConversationResponse(
id=str(conv.id),
organization_id=str(conv.organization_id),
user_id=str(conv.user_id),
title=conv.title,
created_at=conv.created_at.isoformat()
)
for conv in results
]

# standalone/repositories/sqlalchemy_conversation_repo.py
from sqlalchemy.ext.asyncio import AsyncSession
from core.repositories.base import ConversationRepository
from standalone.models.sqlalchemy_models import ConversationModel

class SQLAlchemyConversationRepository(ConversationRepository):
"""SQLAlchemy implementation"""

def __init__(self, session: AsyncSession):
self.session = session

async def get(self, id: uuid.UUID) -> Optional[Conversation]:
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.id == id)
)
model = result.scalar_one_or_none()
return self._to_domain(model) if model else None

async def keyword_search(
self,
organization_id: uuid.UUID,
query: str,
limit: int
) -> List[Conversation]:
# PostgreSQL full-text search
result = await self.session.execute(
select(ConversationModel)
.where(ConversationModel.organization_id == organization_id)
.where(ConversationModel.title_tsv.match(query))
.limit(limit)
)
return [self._to_domain(m) for m in result.scalars()]

# standalone/main.py
from fastapi import FastAPI
from standalone.api.routes import conversations

app = FastAPI(
title="Context Intelligence API",
version="1.0.0",
docs_url="/api/docs"
)

app.include_router(conversations.router, prefix="/api/v1", tags=["conversations"])

# JWT authentication
from standalone.auth.jwt import oauth2_scheme

@app.on_event("startup")
async def startup():
# Initialize database, Weaviate, etc.
pass

CODITECT Mode: Django REST Framework Integration

# coditect/models.py
from django.db import models
from django_multitenant.models import TenantModel
import uuid

class Conversation(TenantModel):
"""Django model with multi-tenant support"""

tenant_id = 'organization_id'

id = models.UUIDField(primary_key=True, default=uuid.uuid4)
organization = models.ForeignKey(
'Organization',
on_delete=models.CASCADE,
related_name='conversations'
)
user = models.ForeignKey('User', on_delete=models.CASCADE)
title = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
db_table = 'conversations'
indexes = [
models.Index(fields=['organization', 'updated_at']),
]

def to_domain(self) -> core.models.Conversation:
"""Convert to framework-agnostic domain model"""
return core.models.Conversation(
id=self.id,
organization_id=self.organization_id,
user_id=self.user_id,
title=self.title,
created_at=self.created_at,
updated_at=self.updated_at
)

# coditect/api/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

from core.services.search_service import SearchService
from coditect.models import Conversation
from coditect.api.serializers import ConversationSerializer

class ConversationViewSet(viewsets.ModelViewSet):
"""Django REST Framework ViewSet"""

permission_classes = [IsAuthenticated]
serializer_class = ConversationSerializer

def get_queryset(self):
# django-multitenant auto-filters by organization_id
return Conversation.objects.all()

@action(detail=False, methods=['get'])
def search(self, request):
"""Hybrid search endpoint"""

query = request.query_params.get('q', '')
limit = int(request.query_params.get('limit', 20))

# Inject SearchService (reuses core business logic)
search_service = SearchService(
conversation_repo=DjangoConversationRepository(),
weaviate_client=get_weaviate_client()
)

results = await search_service.hybrid_search(
organization_id=request.user.organization_id,
query=query,
limit=limit
)

# Convert domain models to Django models for serialization
django_results = [
Conversation.objects.get(id=conv.id)
for conv in results
]

serializer = ConversationSerializer(django_results, many=True)
return Response(serializer.data)

# coditect/repositories/django_conversation_repo.py
from core.repositories.base import ConversationRepository
from coditect.models import Conversation as DjangoConversation

class DjangoConversationRepository(ConversationRepository):
"""Django ORM implementation"""

async def get(self, id: uuid.UUID) -> Optional[Conversation]:
try:
django_model = await DjangoConversation.objects.aget(id=id)
return django_model.to_domain()
except DjangoConversation.DoesNotExist:
return None

async def keyword_search(
self,
organization_id: uuid.UUID,
query: str,
limit: int
) -> List[Conversation]:
# Django ORM full-text search
django_results = await DjangoConversation.objects.filter(
organization_id=organization_id,
title__search=query
)[:limit].all()

return [m.to_domain() for m in django_results]

# coditect/apps.py
from django.apps import AppConfig

class ContextIntelligenceConfig(AppConfig):
name = 'coditect.context_intelligence'
verbose_name = 'Context Intelligence'

def ready(self):
# Register with CODITECT license system
from coditect.licenses import register_feature_flags

register_feature_flags({
'context_semantic_search': ['Pro', 'Enterprise'],
'context_git_correlation': ['Pro', 'Enterprise'],
'context_team_analytics': ['Enterprise']
})

Consequences

Positive

  1. ✅ Maximum Code Reuse (85%)

    • Core business logic shared across both modes
    • Single SearchService, AnalyticsService, CorrelationService
    • Same PostgreSQL schema, Weaviate integration, Celery tasks
  2. ✅ FastAPI Performance for Standalone

    • Async by default (vs. Django sync overhead)
    • ~2x faster response times (50ms vs. 100ms for simple queries)
    • Modern OpenAPI documentation (Swagger UI built-in)
    • ASGI deployment (Uvicorn, Hypercorn)
  3. ✅ Django Integration for CODITECT

    • 60% infrastructure reuse (PostgreSQL, Redis, Celery, auth)
    • Seamless django-multitenant integration (TenantModel pattern)
    • Django admin interface for support team
    • Same look and feel as CODITECT platform
  4. ✅ Independent Evolution

    • Standalone mode can add features without CODITECT approval
    • CODITECT mode can delay features if not needed
    • Clear separation of concerns (API layer vs. business logic)
  5. ✅ Easy Testing

    • Test core services once (framework-agnostic)
    • Test API layers separately (FastAPI vs. Django)
    • Mock repository interface for unit tests
  6. ✅ Deployment Flexibility

    • Standalone: Docker + Kubernetes (lightweight)
    • CODITECT: GCP Cloud Run (existing deployment)
    • Can pivot to standalone-only or CODITECT-only without rewrite

Negative

  1. ⚠️ Dual ORM Maintenance

    • Complexity: Must maintain SQLAlchemy (standalone) AND Django ORM (CODITECT)
    • Mitigation: Repository pattern abstracts ORM differences, core logic unchanged
    • Overhead: ~10% additional development time for API layer
  2. ⚠️ Async/Sync Boundary

    • Django Issue: Django ORM is sync, requires sync_to_async wrapper
    • Example:
      # Django async view
      async def search(self, request):
      # Django ORM is sync!
      results = await sync_to_async(Conversation.objects.filter)(...)
    • Mitigation: Use asgiref.sync_to_async, acceptable 5-10ms overhead
    • Future: Django 5.0+ improves async ORM support
  3. ⚠️ Two Authentication Systems

    • Standalone: JWT tokens (PyJWT library)
    • CODITECT: Django session auth
    • Mitigation: Clear separation, well-documented, no cross-contamination
    • Overhead: ~5% additional security testing
  4. ⚠️ Two Deployment Pipelines

    • Standalone: Dockerfile + Kubernetes manifests
    • CODITECT: Django migration integration
    • Mitigation: Use same CI/CD (GitHub Actions), 90% shared steps
    • Overhead: ~5% additional DevOps work

Risks and Mitigations

RiskLikelihoodImpactMitigation
ORM DivergenceMediumMediumRepository interface + integration tests
Performance DifferenceLowLowBoth use PostgreSQL async, <20ms difference
Auth ConfusionLowMediumClear documentation, separate modules
Deployment ComplexityLowLowShared CI/CD pipeline, 90% overlap

Alternatives Considered

Alternative 1: Django Only (No FastAPI)

Architecture: Use Django REST Framework for both standalone and CODITECT

Pros:

  • ✅ Single framework (simpler maintenance)
  • ✅ No ORM translation layer
  • ✅ Django admin for both modes

Cons:

  • Performance: Django sync overhead (2x slower than FastAPI async)
  • Standalone positioning: FastAPI is "modern Python API" standard (vs. Django "legacy")
  • Documentation: Django REST Framework docs less polished than FastAPI OpenAPI

Why Rejected: Performance matters for real-time conversation capture. FastAPI's async architecture is significantly faster for I/O-bound operations (database queries, Weaviate searches).

Benchmark (100 concurrent users, 1000 requests):

FastAPI:     p50=45ms, p95=120ms, p99=180ms
Django REST: p50=90ms, p95=250ms, p99=400ms

Alternative 2: FastAPI Only (No Django)

Architecture: Use FastAPI for both standalone and CODITECT, custom Django integration

Pros:

  • ✅ Single framework (simpler maintenance)
  • ✅ Best performance for both modes
  • ✅ Modern async patterns everywhere

Cons:

  • CODITECT Integration: Must reimplement Django auth, admin, ORM patterns
  • Code Reuse: 0% reuse of CODITECT infrastructure (vs. 60% with Django)
  • Complexity: Custom Django middleware to integrate FastAPI app
  • Risk: Untested pattern, may break CODITECT assumptions

Why Rejected: CODITECT integration is a core requirement (ADR-001). Using Django for CODITECT mode provides 60% infrastructure reuse, which is critical for rapid deployment.


Alternative 3: Hybrid with Shared Django (Complex)

Architecture: Django for both modes, but use FastAPI for specific high-performance endpoints

Example:

# Django for main app
urlpatterns = [
path('api/conversations/', ConversationViewSet.as_view()),
# FastAPI mounted for search (performance-critical)
path('api/search/', mount_fastapi_app(search_app)),
]

Pros:

  • ✅ Django integration maintained
  • ✅ Performance boost for critical endpoints

Cons:

  • Complexity: Two frameworks in one codebase (nightmare)
  • Auth confusion: JWT (FastAPI) vs. Django session (how to share?)
  • Deployment: Must deploy both ASGI (FastAPI) and WSGI (Django)
  • Testing: Must test both frameworks' behaviors

Why Rejected: Unacceptable complexity. Clear separation (standalone=FastAPI, CODITECT=Django) is simpler.


Success Metrics

Performance Metrics

  • Standalone p95 latency: <100ms for search queries
  • CODITECT p95 latency: <150ms for search queries (acceptable Django overhead)
  • Code reuse: 85%+ shared core logic (measured by LOC)

Development Metrics

  • Time to add feature: <2 days to add to both modes (vs. <1 day single mode)
  • Test coverage: 80%+ for core layer, 70%+ for API layers
  • API consistency: 100% endpoint parity between modes

Integration Metrics

  • CODITECT deployment: Zero breaking changes to existing CODITECT services
  • Django migration: <5 minutes to add context_intelligence app
  • Shared auth: 100% compatibility with django-multitenant

Implementation Plan

Phase 1: Core Layer (Weeks 1-2)

  • Define domain models (dataclasses)
  • Implement services (SearchService, AnalyticsService)
  • Define repository interfaces (ConversationRepository, etc.)
  • Write unit tests (80%+ coverage)

Phase 2: Standalone Mode (Weeks 3-4)

  • Implement FastAPI routes
  • SQLAlchemy repository implementations
  • JWT authentication
  • API documentation (OpenAPI)
  • Integration tests

Phase 3: CODITECT Mode (Weeks 5-6)

  • Django models (extend TenantModel)
  • Django REST Framework ViewSets
  • Django repository implementations
  • Django admin configuration
  • Integration tests

Phase 4: Testing & Deployment (Weeks 7-8)

  • Performance benchmarking (FastAPI vs. Django)
  • End-to-end tests (both modes)
  • Deployment scripts (Docker, Kubernetes, Django migration)
  • Documentation (API docs, deployment guides)

References

FastAPI:

Django REST Framework:

Architecture Patterns:

Related ADRs:

  • ADR-001: Hybrid Architecture (defines 85/15 split)
  • ADR-002: PostgreSQL + Weaviate (database layer both modes use)
  • ADR-004: Multi-Tenant RLS (PostgreSQL isolation strategy)

Status: Proposed Review Date: 2025-12-03 Projected ADR Score: 38/40 (A) Complexity: Medium-High (dual framework) Owner: Architecture Team + CODITECT Integration Team

Next Steps:

  1. Approve dual-framework approach
  2. Implement core layer (Weeks 1-2)
  3. Benchmark FastAPI vs. Django performance
  4. Validate Django integration with CODITECT team