Phase 1 Step 6: License Seat Management API - Implementation Summary
Status: Implementation Complete ✅ Date: December 1, 2025 Implementation Time: ~2 hours
Overview
Implemented production-ready Django REST Framework API endpoints for atomic license seat management with Redis-backed concurrency control, graceful degradation, comprehensive audit logging, and multi-tenant organization scoping.
Files Created/Updated
New Files Created (3 files, ~850 lines)
1. api/v1/serializers/license_seat.py (260 lines)
Purpose: Request/response serializers for seat management endpoints
Serializers:
-
AcquireLicenseRequestSerializer- Validates seat acquisition requests- Validates license key exists, is active, and not expired
- Checks for duplicate active sessions per machine
- Stores license object in context for view access
-
LicenseSessionResponseSerializer- Success response for acquisition- Returns session details with expiration time
- Includes active status calculation
-
HeartbeatResponseSerializer- Heartbeat refresh response- Returns updated expiration time and remaining seconds
-
SeatUnavailableErrorSerializer- 409 Conflict error format- Structured error for "all seats taken" scenario
-
SessionExpiredErrorSerializer- 410 Gone error format- Structured error for expired sessions
Key Features:
- Comprehensive validation with clear error messages
- Cross-field validation for duplicate sessions
- License validation integrated into serializer
- Structured error responses for API consistency
2. api/v1/views/license_seat.py (520 lines)
Purpose: API view implementations for seat acquire, heartbeat, and release
Views:
AcquireLicenseView (POST /api/v1/licenses/acquire)
- Authentication: JWT required (IsAuthenticated)
- Rate Limiting: 10 requests/minute per user
- Functionality:
- Validates request with
AcquireLicenseRequestSerializer - Extracts client IP and user agent
- Creates PostgreSQL session record atomically
- Calls Redis Lua script for atomic seat counting
- Graceful degradation to PostgreSQL if Redis unavailable
- Creates audit log for compliance
- Returns 201 Created with session details
- Returns 409 Conflict if no seats available
- Validates request with
HeartbeatView (PATCH /api/v1/licenses/sessions/{id}/heartbeat)
- Authentication: JWT required
- Rate Limiting: 20 requests/minute per user
- Functionality:
- Retrieves session by UUID
- Validates session not ended and not expired (>6 min inactivity)
- Refreshes Redis TTL (360 seconds)
- Updates PostgreSQL last_heartbeat_at timestamp
- Returns 200 OK with new expiration time
- Returns 410 Gone if session expired
ReleaseLicenseView (DELETE /api/v1/licenses/sessions/{id})
- Authentication: JWT required
- Rate Limiting: None (always allow graceful shutdown)
- Functionality:
- Retrieves session by UUID
- Calls Redis to release seat (decrement counter)
- Marks session as ended in PostgreSQL
- Creates audit log
- Returns 204 No Content
- Idempotent: Multiple calls succeed
Key Features:
- Transaction safety with
@transaction.atomic - Comprehensive error handling with try/except blocks
- IP address extraction with X-Forwarded-For support
- User agent extraction from headers
- Audit logging for all operations
- Graceful Redis degradation (PostgreSQL fallback)
- Clear logging at info/warning/error levels
- OpenAPI documentation with
@extend_schema
3. api/v1/urls.py (Updated)
Changes:
- Updated imports to use new
license_seat.pyviews - Updated URL patterns with clearer comments
- Maintained backward-compatible endpoint paths
Endpoints:
POST /api/v1/licenses/acquire/ # Acquire seat
PATCH /api/v1/licenses/sessions/{session_id}/heartbeat/ # Refresh TTL
DELETE /api/v1/licenses/sessions/{session_id}/ # Release seat
4. api/v1/serializers/__init__.py (Updated)
Changes:
- Added imports for new license seat serializers
- Added to
__all__export list - Organized with clear section comments
Technical Architecture
Request Flow Diagram
Client Request
↓
API View (license_seat.py)
↓
┌─────────────────────────────────────────────────────┐
│ 1. Authentication (JWT) │
│ 2. Rate Limiting (Throttling) │
│ 3. Request Validation (Serializer) │
│ 4. License Validation (Active, Not Expired) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Transaction Block (Atomic) │
│ ├─ PostgreSQL: Create LicenseSession │
│ ├─ Redis: Atomic seat counting (Lua script) │
│ │ └─ Graceful fallback to PostgreSQL if down │
│ ├─ Audit Log: Record action │
│ └─ Return Response (201/200/204) │
└─────────────────────────────────────────────────────┘
↓
Response to Client
Key Implementation Decisions
1. Organization-Based Seat Limits
Decision: Use organization.max_seats instead of license.max_concurrent_seats
Rationale:
- License model migration 0003 removed
max_concurrent_seatsfield - Current architecture uses organization-level seat limits
- Aligns with multi-tenant design (one limit per organization)
- Simplifies subscription management (seats purchased at org level)
Implementation:
max_seats = license_obj.organization.max_seats
2. Graceful Redis Degradation
Decision: Fallback to PostgreSQL counting if Redis unavailable
Rationale:
- Redis provides atomic counting and TTL management (optimal)
- Service should remain available even if Redis down
- PostgreSQL can provide seat counting (less optimal but functional)
Implementation:
try:
# Try Redis first (fast, atomic)
redis_client.acquire_seat(...)
except RedisUnavailableError:
# Fall back to PostgreSQL (slower, but works)
active_sessions = LicenseSession.objects.filter(...).count()
Trade-offs:
- PostgreSQL fallback has race condition risk (less atomic)
- Trade-off: Availability > Perfect Atomicity
- Production should monitor Redis health to avoid fallback mode
3. Transaction Safety
Decision: Wrap all database operations in @transaction.atomic blocks
Rationale:
- Prevents partial state (session created but seat not counted)
- Ensures audit log consistency
- Rollback on Redis errors (delete session if seat acquisition fails)
Implementation:
with transaction.atomic():
session = LicenseSession.objects.create(...)
redis_client.acquire_seat(...)
create_audit_log(...)
4. Rate Limiting Strategy
Decision: Different throttle rates for acquire vs. heartbeat
Rationale:
- Acquisition is expensive (database write + Redis script)
- Heartbeats are frequent (every 3-5 minutes per session)
- Prevent abuse while allowing legitimate usage
Implementation:
class AcquireSeatRateThrottle(UserRateThrottle):
rate = '10/min' # Max 10 seat acquisitions per minute
class HeartbeatRateThrottle(UserRateThrottle):
rate = '20/min' # Max 20 heartbeats per minute
5. Idempotent Release Endpoint
Decision: DELETE endpoint returns 204 even if session already released
Rationale:
- Client apps may call release multiple times (retry logic)
- Graceful shutdown should never fail
- Idempotency simplifies client implementation
Implementation:
if session.ended_at is not None:
# Already released, return success
return Response(status=status.HTTP_204_NO_CONTENT)
Security Considerations
1. Authentication & Authorization
- JWT Required: All endpoints require valid JWT token
- Organization Scoping: Sessions scoped to user's organization via multi-tenancy
- Machine Binding: Sessions tied to specific machine IDs (prevent sharing)
2. Audit Logging
All operations logged to AuditLog model for compliance:
LICENSE_ACQUIRED- Seat allocationLICENSE_RELEASED- Seat release
Audit logs include:
- User ID and organization
- License key and session ID
- IP address and user agent
- Machine ID
- Timestamp
3. Rate Limiting
- Acquire: 10/min per user (prevents rapid seat exhaustion attacks)
- Heartbeat: 20/min per user (allows legitimate frequent heartbeats)
- Release: No limit (always allow graceful shutdown)
4. Input Validation
- License key format validation
- Machine ID required and non-empty
- IP address format validation
- Metadata JSON validation
Error Handling
HTTP Status Codes
| Status Code | Meaning | Use Case |
|---|---|---|
| 201 Created | Success | Seat acquired successfully |
| 200 OK | Success | Heartbeat refreshed |
| 204 No Content | Success | Seat released successfully |
| 400 Bad Request | Validation Error | Invalid license key, missing machine_id |
| 404 Not Found | Resource Missing | Session not found |
| 409 Conflict | Resource Conflict | All seats taken, duplicate session |
| 410 Gone | Resource Expired | Session expired (>6 min inactivity) |
| 429 Too Many Requests | Rate Limit | Exceeded throttle limit |
| 500 Internal Server Error | Server Error | Unexpected error (logged) |
Error Response Format
400 Validation Error:
{
"license_key": ["License expired on 2025-11-01. Please renew."],
"machine_id": ["Machine ID is required"]
}
409 Conflict (All Seats Taken):
{
"error": "license_full",
"message": "All license seats are currently in use",
"max_seats": 5,
"seats_used": 5,
"seats_remaining": 0
}
410 Gone (Session Expired):
{
"error": "session_expired",
"message": "Session expired due to inactivity",
"last_heartbeat_at": "2025-12-01T11:54:00Z",
"expired_at": "2025-12-01T12:00:00Z"
}
Performance Optimizations
1. Redis Lua Scripts
- Atomic Operations: All seat counting in single Redis call
- Pre-loaded Scripts: SHA caching eliminates script transfer overhead
- TTL Management: Automatic expiration after 6 minutes (no cleanup jobs needed)
2. Database Query Optimization
- select_related: Load license and organization in single query
- Indexed Lookups: Session ID lookups use primary key index
- Threshold Queries: Efficient active session counting with indexed last_heartbeat_at
3. Connection Pooling
- Redis: Connection pool with max 20 connections
- PostgreSQL: Django connection pool (default settings)
Testing Strategy
Unit Tests Needed (Next Step)
class TestAcquireLicenseView:
def test_acquire_seat_success(self):
"""Test successful seat acquisition."""
def test_acquire_seat_all_taken(self):
"""Test 409 when all seats in use."""
def test_acquire_seat_expired_license(self):
"""Test 400 for expired license."""
def test_acquire_seat_duplicate_session(self):
"""Test 409 for duplicate machine session."""
def test_acquire_seat_redis_unavailable(self):
"""Test PostgreSQL fallback when Redis down."""
class TestHeartbeatView:
def test_heartbeat_success(self):
"""Test successful heartbeat refresh."""
def test_heartbeat_expired_session(self):
"""Test 410 for expired session."""
def test_heartbeat_session_not_found(self):
"""Test 404 for non-existent session."""
class TestReleaseLicenseView:
def test_release_seat_success(self):
"""Test successful seat release."""
def test_release_seat_idempotent(self):
"""Test multiple releases succeed (idempotent)."""
def test_release_seat_not_found(self):
"""Test 404 for non-existent session."""
Integration Tests Needed
class TestLicenseSessionFlow:
def test_complete_lifecycle(self):
"""Test acquire → heartbeat → release flow."""
def test_automatic_expiration(self):
"""Test session expires after 6 minutes without heartbeat."""
def test_concurrent_acquisitions(self):
"""Test race condition handling (10 threads, 5 seats)."""
Deployment Checklist
- Serializers implemented with validation
- Views implemented with error handling
- URLs configured with correct paths
- Rate limiting configured
- Audit logging integrated
- Transaction safety implemented
- Graceful Redis degradation
- OpenAPI documentation
- Unit tests written (80%+ coverage)
- Integration tests written
- Load tests (100+ concurrent sessions)
- Redis connection configured in production
- Monitoring alerts configured
- API documentation published
Monitoring & Observability
Key Metrics to Track
# Prometheus metrics (to be added)
license_seats_used = Gauge('license_seats_used', 'Current seats in use', ['organization_id'])
license_seats_total = Gauge('license_seats_total', 'Total available seats', ['organization_id'])
license_acquire_requests_total = Counter('license_acquire_requests_total', 'Total acquire requests')
license_acquire_failures_total = Counter('license_acquire_failures_total', 'Failed acquire requests', ['reason'])
session_heartbeat_latency_seconds = Histogram('session_heartbeat_latency_seconds', 'Heartbeat processing time')
redis_fallback_total = Counter('redis_fallback_total', 'Redis unavailable fallback count', ['operation'])
Logging Strategy
Levels:
INFO: Successful operations (seat acquired, released)WARNING: Redis fallback mode, session expired during heartbeatERROR: Unexpected errors, audit log failuresDEBUG: Heartbeat refreshes (verbose)
Key Log Messages:
logger.info(f"Seat acquired: license={license_id}, session={session_id}, seats_used={seats_used}/{max_seats}")
logger.warning(f"Redis unavailable, using PostgreSQL fallback (license={license_id})")
logger.error(f"Unexpected error during seat acquisition: {e}", exc_info=True)
API Documentation Examples
Acquire Seat Example
Request:
POST /api/v1/licenses/acquire/ HTTP/1.1
Host: api.coditect.ai
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"license_key": "CODITECT-2025-A7B3-X9K2",
"machine_id": "hw-id-abc123-xyz789",
"metadata": {
"app_version": "1.0.0",
"os": "Windows 10",
"hostname": "DESKTOP-ABC123"
}
}
Success Response (201):
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"license_key": "CODITECT-2025-A7B3-X9K2",
"started_at": "2025-12-01T12:00:00Z",
"last_heartbeat_at": "2025-12-01T12:00:00Z",
"expires_at": "2025-12-01T12:06:00Z",
"is_active": true,
"machine_id": "hw-id-abc123-xyz789",
"ip_address": "192.168.1.100",
"user_agent": "CODITECT/1.0.0 (Windows 10)",
"metadata": {
"app_version": "1.0.0",
"os": "Windows 10",
"hostname": "DESKTOP-ABC123"
}
}
Heartbeat Example
Request:
PATCH /api/v1/licenses/sessions/550e8400-e29b-41d4-a716-446655440000/heartbeat/ HTTP/1.1
Host: api.coditect.ai
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Success Response (200):
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"expires_at": "2025-12-01T12:11:00Z",
"time_remaining": 360,
"message": "Heartbeat received successfully"
}
Release Seat Example
Request:
DELETE /api/v1/licenses/sessions/550e8400-e29b-41d4-a716-446655440000/ HTTP/1.1
Host: api.coditect.ai
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Success Response (204):
HTTP/1.1 204 No Content
Future Enhancements (Phase 2)
-
Prometheus Metrics Integration
- Add metrics middleware to track seat usage
- Export metrics at
/metricsendpoint - Configure Grafana dashboards
-
WebSocket Support for Real-Time Updates
- Notify clients when seats become available
- Push expiration warnings to clients
- Real-time seat count updates
-
Advanced Rate Limiting
- Per-organization rate limits
- Burst allowance for legitimate spikes
- Redis-backed distributed rate limiting
-
Session Analytics
- Average session duration by organization
- Peak usage times analysis
- Geographic distribution of sessions
-
Automated Testing in CI/CD
- Unit tests in GitHub Actions
- Integration tests with test database
- Load tests with Locust
Success Criteria ✅
Phase 1 Step 6 is complete when:
- Serializers implemented with validation (3 request/response serializers)
- Views implemented with error handling (3 API views)
- URLs configured with correct endpoints
- Rate limiting configured (10/min acquire, 20/min heartbeat)
- Audit logging integrated (LICENSE_ACQUIRED, LICENSE_RELEASED events)
- Transaction safety implemented (@transaction.atomic blocks)
- Graceful Redis degradation (PostgreSQL fallback)
- OpenAPI documentation (@extend_schema decorators)
- IP address and user agent extraction
- Multi-tenant organization scoping
- Unit tests written (>80% coverage) - Next Step
- Integration tests passing - Next Step
- Load tests completed (100+ concurrent sessions) - Next Step
Next Steps (Step 7 Preview)
After completing tests for Step 6, Step 7 will implement client-side integration:
- Background thread for automatic heartbeats (every 3-5 minutes)
- Automatic reconnection on network failure
- Graceful degradation when offline
- Session recovery after app restart
- Client SDK for Python/Node.js/Rust
Document Status: Implementation Complete, Testing Pending ✅ Estimated Testing Time: 2-3 hours Estimated Total Time: 4-5 hours (implementation + testing) Owner: AZ1.AI INC Last Updated: December 1, 2025