Skip to main content

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
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.py views
  • 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_seats field
  • 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 allocation
  • LICENSE_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 CodeMeaningUse Case
201 CreatedSuccessSeat acquired successfully
200 OKSuccessHeartbeat refreshed
204 No ContentSuccessSeat released successfully
400 Bad RequestValidation ErrorInvalid license key, missing machine_id
404 Not FoundResource MissingSession not found
409 ConflictResource ConflictAll seats taken, duplicate session
410 GoneResource ExpiredSession expired (>6 min inactivity)
429 Too Many RequestsRate LimitExceeded throttle limit
500 Internal Server ErrorServer ErrorUnexpected 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 heartbeat
  • ERROR: Unexpected errors, audit log failures
  • DEBUG: 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)

  1. Prometheus Metrics Integration

    • Add metrics middleware to track seat usage
    • Export metrics at /metrics endpoint
    • Configure Grafana dashboards
  2. WebSocket Support for Real-Time Updates

    • Notify clients when seats become available
    • Push expiration warnings to clients
    • Real-time seat count updates
  3. Advanced Rate Limiting

    • Per-organization rate limits
    • Burst allowance for legitimate spikes
    • Redis-backed distributed rate limiting
  4. Session Analytics

    • Average session duration by organization
    • Peak usage times analysis
    • Geographic distribution of sessions
  5. 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