C3 Component Diagram: Django Backend Components
Purpose: Detailed component-level architecture of the Django License Server backend, showing Django apps, models, viewsets, middleware, and service layers with multi-tenant architecture implementation.
Scope: Django application layer (backend container from C2)
Related Diagrams:
- C2: Container Diagram - Shows Django Backend as a container
- C4-01: TenantModel Hierarchy - Class-level detail of tenant models
- ADR-007: Django Multi-Tenant Architecture - Complete implementation
Mermaid Component Diagram
Component Details
1. Django Apps Structure
apps.tenants
Purpose: Core tenant management and multi-tenant infrastructure
Components:
models.pyTenant- Organization model (name, slug, plan_tier, stripe_customer_id)TenantModel- Abstract base class for tenant-scoped models
context.pyset_current_tenant()- Set tenant context for requestget_current_tenant()- Retrieve current tenantclear_current_tenant()- Clear tenant contexttenant_context- Context manager for tenant-scoped operations
middleware.pyTenantMiddleware- Sets tenant from authenticated user
viewsets.pyTenantViewSet- Base viewset with automatic tenant filtering
Key Responsibilities:
- Tenant context management (thread-safe via ContextVar)
- PostgreSQL session variable management (
set_current_tenant()SQL function) - Automatic tenant filtering for all queries
- Middleware integration with authentication
apps.users
Purpose: User management with tenant-scoped authentication
Components:
models.pyUser(TenantModel, AbstractBaseUser)- Custom user model with tenant FKUserRole- Enum: owner, admin, member, viewer
serializers.pyUserSerializer- User CRUD serializationUserCreateSerializer- User registration with password hashing
viewsets.pyUserViewSet(TenantViewSet)- User management API- Role-based filtering (admins see all, members see self)
permissions.pyIsOwnerOrAdmin- Custom permission for user management
Key Responsibilities:
- User authentication (email + password)
- Tenant-scoped user management
- Role-based access control (RBAC)
- Permission validation per tenant
Example Code:
class User(TenantModel, AbstractBaseUser):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
email = models.EmailField(unique=False) # Unique per tenant, not global
role = models.CharField(choices=UserRole.choices, default='member')
class Meta:
unique_together = [('tenant', 'email')]
apps.licenses
Purpose: License session management and validation
Components:
models.pyLicenseSession(TenantModel)- Active license sessionsLicenseType- Enum: free, pro, team, enterpriseSessionStatus- Enum: active, expired, revoked
serializers.pyLicenseSessionSerializer- Session CRUDLicenseValidationSerializer- Validation request/response
viewsets.pyLicenseSessionViewSet(TenantViewSet)- License API@actionmethods: validate, heartbeat, release
services.pyLicenseService- License validation logic with RedisSessionService- Session lifecycle management
Key Responsibilities:
- License validation (<100ms p99)
- Session heartbeat tracking (Redis TTL 6 min)
- Atomic seat counting (Redis Lua scripts)
- Cloud KMS license signing (RSA-4096)
Example Code:
class LicenseSession(TenantModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
session_token = models.CharField(max_length=255, unique=True)
machine_id = models.CharField(max_length=255)
license_type = models.CharField(choices=LicenseType.choices)
expires_at = models.DateTimeField()
def validate(self):
"""Validate session and update last_validated_at."""
if not self.is_valid:
return False
self.last_validated_at = timezone.now()
self.save(update_fields=['last_validated_at'])
return True
apps.projects
Purpose: Project management for license organization
Components:
models.pyProject(TenantModel)- User projectsProjectStatus- Enum: active, archived
serializers.pyProjectSerializer- Project CRUD
viewsets.pyProjectViewSet(TenantViewSet)- Project API- Automatic owner assignment to current user
Key Responsibilities:
- Project CRUD operations
- Tenant quota enforcement (max_projects)
- Project archiving/restoration
apps.audit
Purpose: Comprehensive audit logging for compliance
Components:
models.pyAuditLog(TenantModel)- Audit trail entries
services.pyAuditService- Audit log creation with context
viewsets.pyAuditLogViewSet(TenantViewSet)- Read-only audit API
Key Responsibilities:
- Automatic audit logging (create, update, delete, login)
- Actor tracking (user_id, user_email, ip_address, user_agent)
- Before/after change tracking (JSON diff)
- Compliance reporting (SOC 2, GDPR, HIPAA)
Example Usage:
AuditLog.log(
action='create',
resource_type='project',
resource_id=project.id,
user=request.user,
metadata={'project_name': project.name},
request=request
)
apps.auth
Purpose: Authentication and JWT token management
Components:
authentication.pyJWTAuthentication- Custom JWT auth backend
serializers.pyLoginSerializer- Email/password loginTokenSerializer- JWT token response
views.pyLoginView- JWT token generationRefreshView- Token refreshLogoutView- Token revocation
Key Responsibilities:
- JWT token generation and validation
- Integration with Identity Platform (OAuth2/OIDC)
- Token refresh and revocation
- Multi-provider auth (Google, GitHub)
2. Middleware Layer
Execution Order (CRITICAL):
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 1. HTTPS enforcement
'corsheaders.middleware.CorsMiddleware', # 2. CORS headers
'django.contrib.sessions.middleware.SessionMiddleware', # 3. Session support
'django.middleware.common.CommonMiddleware', # 4. Common processing
'django.middleware.csrf.CsrfViewMiddleware', # 5. CSRF protection
'django.contrib.auth.middleware.AuthenticationMiddleware', # 6. Set request.user
'apps.tenants.middleware.TenantMiddleware', # 7. Set request.tenant ⭐
'django.contrib.messages.middleware.MessageMiddleware', # 8. Messages
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 9. Clickjacking protection
]
CRITICAL: TenantMiddleware MUST come AFTER AuthenticationMiddleware because it requires request.user to be set.
TenantMiddleware Workflow
class TenantMiddleware(MiddlewareMixin):
def process_request(self, request):
clear_current_tenant() # Clear previous context
if request.user and request.user.is_authenticated:
if hasattr(request.user, 'tenant'):
set_current_tenant(request.user.tenant) # Set Python + PostgreSQL context
request.tenant = request.user.tenant
def process_response(self, request, response):
clear_current_tenant() # Always clear after request
return response
Side Effects:
- Sets Python
ContextVar[Tenant]for application code - Sets PostgreSQL session variable
app.current_tenant_idfor RLS enforcement - Attaches
request.tenantfor easy access in views
3. Tenant Context Management (Thread-Safe)
Implementation:
# apps/tenants/context.py
from contextvars import ContextVar
from typing import Optional
_current_tenant: ContextVar[Optional[Tenant]] = ContextVar('current_tenant', default=None)
def set_current_tenant(tenant: Optional[Tenant]) -> None:
"""Set current tenant for this context (thread-safe)."""
_current_tenant.set(tenant)
# Also set PostgreSQL session variable for RLS
if tenant:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT set_current_tenant(%s)", [str(tenant.id)])
def get_current_tenant() -> Optional[Tenant]:
"""Get current tenant from context."""
return _current_tenant.get()
def clear_current_tenant() -> None:
"""Clear current tenant context."""
_current_tenant.set(None)
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT clear_current_tenant()")
Context Manager for Async Tasks:
class tenant_context:
"""Context manager for temporarily setting tenant context."""
def __init__(self, tenant: Optional[Tenant]):
self.tenant = tenant
self.previous_tenant = None
def __enter__(self):
self.previous_tenant = get_current_tenant()
set_current_tenant(self.tenant)
return self.tenant
def __exit__(self, exc_type, exc_val, exc_tb):
set_current_tenant(self.previous_tenant)
# Usage in Celery tasks
@shared_task
def send_license_expiry_email(license_id):
license = LicenseSession.objects.get(id=license_id)
with tenant_context(license.tenant):
# All queries here are tenant-scoped
user = license.user
EmailService.send_expiry_notification(user.email)
4. Model Layer - django-multitenant Integration
TenantModel Base Class
# apps/tenants/models.py
from django_multitenant.models import TenantModel as BaseTenantModel
class TenantModel(BaseTenantModel):
"""
Base class for all tenant-scoped models.
Features:
- Automatic tenant FK enforcement
- Auto-filtering by current tenant
- Save validation (requires tenant context)
"""
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
related_name='%(class)ss', # e.g., tenant.users, tenant.projects
db_index=True
)
class Meta:
abstract = True
def save(self, *args, **kwargs):
# Auto-set tenant from context if not provided
if not self.tenant_id:
from .context import get_current_tenant
current_tenant = get_current_tenant()
if current_tenant:
self.tenant_id = current_tenant.id
else:
raise ValueError(
f"Cannot save {self.__class__.__name__} without tenant context. "
"Set tenant explicitly or use TenantMiddleware."
)
super().save(*args, **kwargs)
Automatic Filtering:
# When tenant context is set, ALL queries auto-filter
set_current_tenant(my_tenant)
# These queries automatically filter to my_tenant:
User.objects.all() # Returns only my_tenant's users
Project.objects.filter(status='active') # Returns only my_tenant's active projects
LicenseSession.objects.get(id=some_id) # Only returns if session belongs to my_tenant
# Trying to access another tenant's data returns empty queryset or DoesNotExist
PostgreSQL Row-Level Security Enforcement:
Even if application code has bugs, PostgreSQL RLS prevents cross-tenant access:
-- RLS Policy on users table
CREATE POLICY users_tenant_isolation ON users
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
-- Result: Raw SQL queries ALSO respect tenant isolation
SELECT * FROM users; -- Only returns current tenant's users
UPDATE users SET username = 'hacked' WHERE id = 'other-tenant-user-id'; -- Silently fails (0 rows affected)
5. ViewSet Layer - Django REST Framework
TenantViewSet Base Class
# apps/api/viewsets.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from apps.tenants.context import get_current_tenant
class TenantViewSet(viewsets.ModelViewSet):
"""
Base viewset for tenant-scoped resources.
Features:
- Automatic tenant filtering
- Automatic tenant assignment on create
- Permission enforcement
"""
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Filter queryset to current tenant."""
queryset = super().get_queryset()
tenant = get_current_tenant()
if tenant:
# Explicit filter for clarity (also enforced by RLS)
queryset = queryset.filter(tenant=tenant)
return queryset
def perform_create(self, serializer):
"""Set tenant on create."""
tenant = get_current_tenant()
if not tenant:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("No tenant context available")
serializer.save(tenant=tenant)
Example ViewSets
UserViewSet:
class UserViewSet(TenantViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
# Role-based filtering
if user.is_owner or user.is_admin:
return queryset # See all users in tenant
else:
return queryset.filter(id=user.id) # See only self
LicenseSessionViewSet:
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class LicenseSessionViewSet(TenantViewSet):
queryset = LicenseSession.objects.all()
serializer_class = LicenseSessionSerializer
@action(detail=False, methods=['post'])
def validate(self, request):
"""Validate license and acquire seat."""
serializer = LicenseValidationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Use service layer for business logic
result = LicenseService.validate_license(
user=request.user,
machine_id=serializer.validated_data['machine_id'],
tenant=get_current_tenant()
)
return Response(result, status=status.HTTP_200_OK)
@action(detail=True, methods=['post'])
def heartbeat(self, request, pk=None):
"""Update session heartbeat."""
session = self.get_object()
session.validate()
return Response({'status': 'ok', 'expires_at': session.expires_at})
@action(detail=True, methods=['post'])
def release(self, request, pk=None):
"""Release license seat."""
session = self.get_object()
SessionService.release_seat(session)
return Response({'status': 'released'})
6. Service Layer
Purpose: Encapsulate business logic, external integrations, and complex operations.
LicenseService
# apps/licenses/services.py
from django.utils import timezone
from apps.tenants.context import get_current_tenant
import redis
import uuid
class LicenseService:
"""License validation and seat management."""
@classmethod
def validate_license(cls, user, machine_id, tenant):
"""
Validate license and acquire seat.
Steps:
1. Check tenant license quota
2. Atomic seat acquisition in Redis
3. Create session in PostgreSQL
4. Sign license with Cloud KMS
Returns:
dict: {
'valid': bool,
'session_token': str,
'signed_license': str,
'expires_at': datetime,
'features': list
}
"""
from apps.licenses.models import LicenseSession
# 1. Check tenant quota
active_sessions = LicenseSession.objects.filter(
tenant=tenant,
status='active'
).count()
if active_sessions >= tenant.max_users:
return {'valid': False, 'error': 'No available seats'}
# 2. Atomic seat acquisition in Redis
redis_client = get_redis_client()
seat_key = f"tenant:{tenant.id}:seats"
# Lua script for atomic increment with limit check
lua_script = """
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
local max = tonumber(ARGV[1])
if current < max then
redis.call('INCR', KEYS[1])
return 1
else
return 0
end
"""
acquired = redis_client.eval(lua_script, 1, seat_key, tenant.max_users)
if not acquired:
return {'valid': False, 'error': 'No available seats (race condition)'}
# 3. Create session
session_token = str(uuid.uuid4())
expires_at = timezone.now() + timezone.timedelta(hours=8)
session = LicenseSession.objects.create(
tenant=tenant,
user=user,
session_token=session_token,
machine_id=machine_id,
license_type=tenant.plan_tier,
features=cls._get_features_for_tier(tenant.plan_tier),
expires_at=expires_at
)
# 4. Sign license with Cloud KMS
signed_license = cls._sign_license(session)
# 5. Set Redis TTL for automatic cleanup (6 min)
session_key = f"session:{session.id}"
redis_client.setex(session_key, 360, session_token)
return {
'valid': True,
'session_token': session_token,
'signed_license': signed_license,
'expires_at': expires_at,
'features': session.features
}
@classmethod
def _sign_license(cls, session):
"""Sign license with Cloud KMS RSA-4096."""
from google.cloud import kms
client = kms.KeyManagementServiceClient()
key_name = f"projects/{PROJECT_ID}/locations/global/keyRings/licenses/cryptoKeys/signing/cryptoKeyVersions/1"
# Create license payload
payload = {
'session_id': str(session.id),
'user_id': str(session.user_id),
'tenant_id': str(session.tenant_id),
'machine_id': session.machine_id,
'expires_at': session.expires_at.isoformat(),
'features': session.features
}
import json
import base64
message = json.dumps(payload).encode('utf-8')
# Sign with KMS
response = client.asymmetric_sign(
request={'name': key_name, 'digest': {'sha256': hashlib.sha256(message).digest()}}
)
signature = base64.b64encode(response.signature).decode('utf-8')
return {
'payload': payload,
'signature': signature,
'algorithm': 'RSA_SIGN_PKCS1_4096_SHA256'
}
SessionService
class SessionService:
"""Session lifecycle management."""
@classmethod
def release_seat(cls, session):
"""Release license seat and decrement Redis counter."""
from apps.licenses.models import SessionStatus
# 1. Update session status
session.status = SessionStatus.REVOKED
session.revoked_at = timezone.now()
session.save()
# 2. Decrement Redis seat counter
redis_client = get_redis_client()
seat_key = f"tenant:{session.tenant_id}:seats"
redis_client.decr(seat_key)
# 3. Delete Redis session key
session_key = f"session:{session.id}"
redis_client.delete(session_key)
# 4. Audit log
AuditLog.log(
action='release',
resource_type='license_session',
resource_id=session.id,
user=session.user,
metadata={'machine_id': session.machine_id}
)
7. Background Tasks - Celery
Celery Configuration:
# config/celery.py
from celery import Celery
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.production')
app = Celery('license_server')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# Redis as broker and result backend
app.conf.broker_url = 'redis://redis:6379/0'
app.conf.result_backend = 'redis://redis:6379/0'
Periodic Tasks:
# apps/licenses/tasks.py
from celery import shared_task
from apps.tenants.context import tenant_context
from apps.tenants.models import Tenant
from apps.licenses.models import LicenseSession, SessionStatus
from django.utils import timezone
@shared_task
def session_cleanup_task():
"""Expire old sessions (runs every 5 minutes)."""
# Process all tenants
for tenant in Tenant.objects.filter(status='active'):
with tenant_context(tenant):
# Find expired sessions
expired_sessions = LicenseSession.objects.filter(
status=SessionStatus.ACTIVE,
expires_at__lt=timezone.now()
)
for session in expired_sessions:
session.status = SessionStatus.EXPIRED
session.save()
# Release seat in Redis
SessionService.release_seat(session)
@shared_task
def audit_archive_task():
"""Archive audit logs older than 90 days (runs daily)."""
from apps.audit.models import AuditLog
cutoff = timezone.now() - timezone.timedelta(days=90)
for tenant in Tenant.objects.all():
with tenant_context(tenant):
old_logs = AuditLog.objects.filter(created_at__lt=cutoff)
# Archive to Cloud Storage
# ... (implementation details)
old_logs.delete()
@shared_task
def metrics_update_task():
"""Update Prometheus metrics (runs every 1 minute)."""
from apps.tenants.metrics import update_tenant_metrics
for tenant in Tenant.objects.filter(status='active'):
update_tenant_metrics(tenant)
Celery Beat Schedule:
# config/settings/base.py
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'session-cleanup': {
'task': 'apps.licenses.tasks.session_cleanup_task',
'schedule': 300.0, # Every 5 minutes
},
'audit-archive': {
'task': 'apps.licenses.tasks.audit_archive_task',
'schedule': crontab(hour=2, minute=0), # 2 AM daily
},
'metrics-update': {
'task': 'apps.licenses.tasks.metrics_update_task',
'schedule': 60.0, # Every 1 minute
},
}
8. Utilities
validators.py
# apps/common/validators.py
from django.core.exceptions import ValidationError
import re
def validate_machine_id(value):
"""Validate machine ID format (UUID or SHA-256 hash)."""
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
sha256_pattern = r'^[0-9a-f]{64}$'
if not (re.match(uuid_pattern, value) or re.match(sha256_pattern, value)):
raise ValidationError('Invalid machine_id format')
def validate_tenant_slug(value):
"""Validate tenant slug (lowercase alphanumeric + hyphens)."""
if not re.match(r'^[a-z0-9-]+$', value):
raise ValidationError('Slug must be lowercase alphanumeric with hyphens')
permissions.py
# apps/common/permissions.py
from rest_framework import permissions
class IsOwnerOrAdmin(permissions.BasePermission):
"""Allow access to tenant owners and admins."""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
if not request.user.is_authenticated:
return False
# Owners and admins have full access
if request.user.is_owner or request.user.is_admin:
return True
# Members can only access their own objects
if hasattr(obj, 'user'):
return obj.user == request.user
if hasattr(obj, 'owner'):
return obj.owner == request.user
return False
class IsTenantMember(permissions.BasePermission):
"""Allow access to any tenant member."""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and hasattr(request.user, 'tenant')
exceptions.py
# apps/common/exceptions.py
from rest_framework.exceptions import APIException
from rest_framework import status
class NoSeatsAvailable(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = 'No license seats available'
default_code = 'no_seats_available'
class TenantQuotaExceeded(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = 'Tenant quota exceeded'
default_code = 'quota_exceeded'
class InvalidLicenseSignature(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Invalid license signature'
default_code = 'invalid_signature'
Data Flow Examples
License Validation Flow
1. Client → POST /api/v1/licenses/validate
{
"machine_id": "abc123...",
"hardware_fingerprint": {...}
}
2. Middleware → TenantMiddleware
- AuthenticationMiddleware sets request.user
- TenantMiddleware sets request.tenant from request.user.tenant
- set_current_tenant(tenant) → Python ContextVar + PostgreSQL session var
3. ViewSet → LicenseSessionViewSet.validate()
- Permission check: IsAuthenticated
- Serializer validation
- Call LicenseService.validate_license()
4. Service → LicenseService.validate_license()
- Check PostgreSQL quota (tenant.max_users)
- Atomic seat acquisition in Redis (Lua script)
- Create LicenseSession in PostgreSQL (auto-scoped to tenant)
- Sign license with Cloud KMS (RSA-4096)
5. Response → Client
{
"valid": true,
"session_token": "uuid...",
"signed_license": {
"payload": {...},
"signature": "base64...",
"algorithm": "RSA_SIGN_PKCS1_4096_SHA256"
},
"expires_at": "2025-12-01T12:00:00Z",
"features": ["feature1", "feature2"]
}
6. Middleware → TenantMiddleware.process_response()
- clear_current_tenant() → Python + PostgreSQL
Celery Background Task Flow
1. Celery Beat → Triggers session_cleanup_task every 5 minutes
2. Task Execution → session_cleanup_task()
- Iterate all active tenants
3. Per Tenant → with tenant_context(tenant):
- set_current_tenant(tenant)
- Query expired sessions (Django ORM auto-filters by tenant)
- Update session status to EXPIRED
- Release seat in Redis (decrement counter)
- clear_current_tenant()
4. Result → All tenants processed independently
Technology Integration
Django REST Framework Configuration
# config/settings/base.py
REST_FRAMEWORK = {
# Authentication
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'apps.auth.authentication.JWTAuthentication',
],
# Permissions
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
# Pagination
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 50,
'MAX_PAGE_SIZE': 1000,
# Filtering
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
# Throttling
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '10000/hour',
},
}
django-multitenant Configuration
# config/settings/base.py
INSTALLED_APPS = [
# ...
'django_multitenant',
# ...
]
# Tenant FK field name (default: 'tenant')
MULTITENANT_RELATIVE_NAME = 'tenant'
Testing Strategy
Unit Tests
# apps/tenants/tests/test_context.py
def test_tenant_context_isolation():
"""Verify tenant context is isolated per request."""
tenant1 = Tenant.objects.create(name="Tenant 1")
tenant2 = Tenant.objects.create(name="Tenant 2")
set_current_tenant(tenant1)
assert get_current_tenant() == tenant1
set_current_tenant(tenant2)
assert get_current_tenant() == tenant2
clear_current_tenant()
assert get_current_tenant() is None
Integration Tests
# apps/api/tests/test_license_api.py
def test_license_validation_endpoint():
"""Test license validation API with tenant isolation."""
client = APIClient()
client.force_authenticate(user=self.user1)
response = client.post('/api/v1/licenses/validate', {
'machine_id': 'test-machine-123'
})
assert response.status_code == 200
assert response.data['valid'] is True
assert 'signed_license' in response.data
Performance Tests
# apps/tenants/tests/test_performance.py
def test_query_performance_with_rls():
"""Measure query performance with RLS enabled."""
timings = []
for tenant in self.tenants[:10]:
set_current_tenant(tenant)
start = time.perf_counter()
sessions = LicenseSession.objects.filter(status='active').select_related('user').all()
count = len(sessions)
elapsed = (time.perf_counter() - start) * 1000
timings.append(elapsed)
avg_time = statistics.mean(timings)
assert avg_time < 50, "Average query time should be <50ms"
Deployment Considerations
Environment Variables
# Django settings
DJANGO_SETTINGS_MODULE=config.settings.production
SECRET_KEY=<secure-random-key>
# Database (Cloud SQL)
DB_HOST=/cloudsql/coditect-cloud-infra:us-central1:licenses-db
DB_PORT=5432
DB_NAME=licenses
DB_USER=app_user
DB_PASSWORD=<from-secret-manager>
# Redis (Memorystore)
REDIS_HOST=10.0.0.3
REDIS_PORT=6379
# Celery
CELERY_BROKER_URL=redis://10.0.0.3:6379/0
CELERY_RESULT_BACKEND=redis://10.0.0.3:6379/0
# GCP
GCP_PROJECT_ID=coditect-cloud-infra
GCP_REGION=us-central1
KMS_KEY_RING=licenses
KMS_SIGNING_KEY=signing
# CORS
CORS_ALLOWED_ORIGINS=https://app.coditect.ai,https://admin.coditect.ai
Kubernetes Deployment
# kubernetes/django-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: django-backend
spec:
replicas: 3
template:
spec:
containers:
- name: django
image: gcr.io/coditect-cloud-infra/license-server:latest
command: ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
env:
- name: DJANGO_SETTINGS_MODULE
value: config.settings.production
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
Monitoring & Observability
Prometheus Metrics
# Exposed at /metrics endpoint
# Request metrics per tenant
tenant_requests_total{tenant_id="...", method="POST", path="/api/v1/licenses/validate", status="200"} 1234
tenant_request_duration_milliseconds{tenant_id="...", method="POST", path="/api/v1/licenses/validate"} 45.2
# License metrics per tenant
tenant_active_users{tenant_id="..."} 87
tenant_license_sessions{tenant_id="...", status="active"} 45
Logging
# Structured JSON logging
{
"timestamp": "2025-11-30T12:34:56Z",
"level": "INFO",
"logger": "apps.licenses.services",
"message": "License validated successfully",
"tenant_id": "uuid...",
"user_id": "uuid...",
"machine_id": "abc123...",
"session_id": "uuid...",
"duration_ms": 42.5
}
Related Documentation
- C2 Container Diagram: c2-container-diagram.md
- C4 Code Diagrams: c4-01-tenantmodel-hierarchy.md
- ADR-007: Django Multi-Tenant Architecture
- Sequence Diagrams: diagrams/sequences/
Last Updated: 2025-11-30 Diagram Type: C3 Component (Mermaid) Scope: Django Backend - Application Components