Skip to main content

Phase 1 Step 3: License Generation - Design Document

Date: December 1, 2025 Author: AI Assistant Status: Implementation Ready


Overview

Automatically generate and manage CODITECT licenses triggered by Stripe subscription payments. Licenses are created when customers complete checkout and updated/deactivated based on subscription lifecycle events.


Goals

  1. Automatic License Generation: Create licenses immediately after successful Stripe payment
  2. License Key Format: Generate unique, secure license keys (format: CODITECT-XXXX-XXXX-XXXX)
  3. Subscription Integration: Link licenses to Stripe subscriptions for automatic expiry updates
  4. License Management API: Provide endpoints for customers to view and manage their licenses
  5. Email Delivery: Send license details via email after generation (Phase 1 Step 5)

License Key Format

Structure

CODITECT-{YEAR}-{RANDOM4}-{RANDOM4}

Example: CODITECT-2025-A7B3-X9K2

Generation Rules

  • Prefix: Always CODITECT-
  • Year: 4-digit year of issuance (e.g., 2025)
  • Random Segments: Two 4-character alphanumeric segments (uppercase, no ambiguous characters: 0,O,I,1)
  • Character Set: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (32 characters - excludes 0, O, I, 1)
  • Uniqueness: Check database before issuing to prevent duplicates
  • Entropy: ~20 bits per segment (32^4), 40 bits total = 1 trillion possible keys per year

Database Schema Updates

License Model Extensions

The existing License model already has most fields we need. We'll add Stripe integration fields:

# Add to licenses/models.py - License model

# Stripe Integration (Phase 1 Step 3)
stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True, db_index=True)
stripe_price_id = models.CharField(max_length=255, null=True, blank=True)
stripe_checkout_session_id = models.CharField(max_length=255, null=True, blank=True)

# Auto-renewal tracking
auto_renew = models.BooleanField(default=True)
cancel_at_period_end = models.BooleanField(default=False)

Organization Model Extensions

Link organizations to their active licenses:

# Add to tenants/models.py - Organization model

# Primary license (Phase 1 Step 3)
active_license = models.ForeignKey(
'licenses.License',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='primary_for_orgs'
)

License Tier Mapping

Map Stripe plans to license tiers:

Stripe PlanLicense TierFeatures
FreeN/ANo license needed (free tier)
ProPROMarketplace, Analytics, Priority Support
EnterpriseENTERPRISEAll Pro features + SSO, Custom Integrations, Dedicated Support

API Endpoints

1. List Organization Licenses

GET /api/v1/licenses/

Authentication: Required (JWT)
Permission: Organization member

Response (200 OK):
{
"licenses": [
{
"id": "uuid-123",
"key_string": "CODITECT-2025-A7B3-X9K2",
"tier": "PRO",
"features": ["marketplace", "analytics"],
"expiry_date": "2026-01-01T00:00:00Z",
"is_active": true,
"is_expired": false,
"is_valid": true,
"created_at": "2025-12-01T10:00:00Z",
"auto_renew": true,
"cancel_at_period_end": false,
"stripe_subscription_id": "sub_xxx"
}
],
"active_license": {
"id": "uuid-123",
"key_string": "CODITECT-2025-A7B3-X9K2",
"tier": "PRO"
}
}

2. Get License Details

GET /api/v1/licenses/{license_id}/

Authentication: Required (JWT)
Permission: Organization member

Response (200 OK):
{
"id": "uuid-123",
"key_string": "CODITECT-2025-A7B3-X9K2",
"tier": "PRO",
"features": ["marketplace", "analytics"],
"expiry_date": "2026-01-01T00:00:00Z",
"is_active": true,
"is_expired": false,
"is_valid": true,
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2025-12-01T10:00:00Z",
"organization": {
"id": "org-uuid",
"name": "Acme Corp"
},
"subscription": {
"stripe_subscription_id": "sub_xxx",
"stripe_price_id": "price_xxx",
"auto_renew": true,
"cancel_at_period_end": false
},
"usage": {
"seats_purchased": 5,
"seats_used": 3,
"seats_remaining": 2,
"active_sessions": 3
}
}

3. Validate License Key

POST /api/v1/licenses/validate/

Authentication: Optional (public endpoint for offline validation)

Request Body:
{
"key_string": "CODITECT-2025-A7B3-X9K2"
}

Response (200 OK):
{
"valid": true,
"tier": "PRO",
"features": ["marketplace", "analytics"],
"expiry_date": "2026-01-01T00:00:00Z",
"organization": {
"id": "org-uuid",
"name": "Acme Corp"
}
}

Response (200 OK - Invalid):
{
"valid": false,
"reason": "license_expired" // or "license_not_found", "license_inactive"
}

4. Download License File

GET /api/v1/licenses/{license_id}/download/

Authentication: Required (JWT)
Permission: Organization member

Response (200 OK):
Content-Type: application/json
Content-Disposition: attachment; filename="coditect-license-XXXX.json"

{
"license_key": "CODITECT-2025-A7B3-X9K2",
"organization": "Acme Corp",
"organization_id": "org-uuid",
"tier": "PRO",
"features": ["marketplace", "analytics"],
"expiry_date": "2026-01-01T00:00:00Z",
"issued_at": "2025-12-01T10:00:00Z",
"signed": {
"payload": { ... },
"signature": "base64-signature",
"algorithm": "RS256",
"key_id": "projects/.../keys/..."
}
}

License Generation Flow

Trigger: Stripe Checkout Completed

Event: checkout.session.completed

Webhook Handler Logic:

def _handle_checkout_completed(self, event_data):
"""Handle successful checkout session - Generate license."""
customer_id = event_data.get('customer_id')
subscription_id = event_data.get('subscription_id')
checkout_session_id = event_data.get('checkout_session_id')

if not customer_id or not subscription_id:
return

try:
# Get organization
organization = Organization.objects.get(stripe_customer_id=customer_id)

# Update subscription ID
organization.stripe_subscription_id = subscription_id
organization.save()

# Fetch subscription details from Stripe
subscription = StripeService.retrieve_subscription(subscription_id)

# Determine license tier from Stripe price ID
price_id = subscription.get('price_id')
tier = self._map_price_to_tier(price_id)

if tier == 'FREE':
# Free plan doesn't need license
return

# Generate license
license = self._generate_license(
organization=organization,
tier=tier,
subscription=subscription,
checkout_session_id=checkout_session_id
)

# Set as active license for organization
organization.active_license = license
organization.save()

# Create audit log
create_audit_log(
organization=organization,
user=None, # System action
action='LICENSE_GENERATED',
resource_type='license',
resource_id=license.id,
metadata={
'license_key': license.key_string,
'tier': tier,
'stripe_subscription_id': subscription_id,
}
)

logger.info(f"License generated: {license.key_string} for org: {organization.id}")

# TODO Phase 1 Step 5: Send license email via SendGrid

except Organization.DoesNotExist:
logger.error(f"Organization not found for customer: {customer_id}")
except Exception as e:
logger.error(f"License generation failed: {str(e)}", exc_info=True)


def _generate_license(self, organization, tier, subscription, checkout_session_id):
"""Generate a new license with unique key."""
from licenses.services import LicenseService

# Generate unique license key
license_key = LicenseService.generate_unique_key()

# Determine features based on tier
features = self._get_tier_features(tier)

# Calculate expiry date (subscription period end)
expiry_date = datetime.fromtimestamp(
subscription['current_period_end'],
tz=timezone.utc
)

# Create license
license = License.objects.create(
organization=organization,
key_string=license_key,
tier=tier,
features=features,
expiry_date=expiry_date,
is_active=True,
stripe_subscription_id=subscription['subscription_id'],
stripe_price_id=subscription['price_id'],
stripe_checkout_session_id=checkout_session_id,
auto_renew=True,
cancel_at_period_end=False,
created_by=None, # System-generated
)

return license


def _map_price_to_tier(self, price_id):
"""Map Stripe price ID to license tier."""
plans = StripeService.get_plans()

for plan in plans:
if plan.get('stripe_price_id') == price_id:
return plan['id'].upper()

return 'FREE' # Default fallback


def _get_tier_features(self, tier):
"""Get feature list for license tier."""
tier_features = {
'PRO': ['marketplace', 'analytics', 'priority_support'],
'ENTERPRISE': ['marketplace', 'analytics', 'priority_support', 'sso', 'custom_integrations', 'dedicated_support'],
}

return tier_features.get(tier, [])

Update: Subscription Period Renewed

Event: customer.subscription.updated or invoice.payment_succeeded

Handler Logic:

def _handle_subscription_updated(self, event_data):
"""Handle subscription update - Update license expiry."""
subscription_id = event_data.get('subscription_id')

if not subscription_id:
return

try:
# Find license by subscription ID
license = License.objects.filter(
stripe_subscription_id=subscription_id,
is_active=True
).first()

if not license:
logger.warning(f"No active license found for subscription: {subscription_id}")
return

# Update expiry date
if event_data.get('current_period_end'):
license.expiry_date = datetime.fromtimestamp(
event_data['current_period_end'],
tz=timezone.utc
)
license.save()

logger.info(f"License expiry updated: {license.key_string}, new expiry: {license.expiry_date}")

except Exception as e:
logger.error(f"License update failed: {str(e)}", exc_info=True)

Deactivate: Subscription Canceled

Event: customer.subscription.deleted

Handler Logic:

def _handle_subscription_deleted(self, event_data):
"""Handle subscription deletion - Deactivate license."""
subscription_id = event_data.get('subscription_id')

if not subscription_id:
return

try:
# Find license by subscription ID
license = License.objects.filter(
stripe_subscription_id=subscription_id,
is_active=True
).first()

if not license:
logger.warning(f"No active license found for subscription: {subscription_id}")
return

# Deactivate license
license.is_active = False
license.save()

# Remove as active license from organization
organization = license.organization
if organization.active_license == license:
organization.active_license = None
organization.save()

# Create audit log
create_audit_log(
organization=organization,
user=None,
action='LICENSE_DEACTIVATED',
resource_type='license',
resource_id=license.id,
metadata={
'license_key': license.key_string,
'reason': 'subscription_canceled',
}
)

logger.info(f"License deactivated: {license.key_string} for org: {organization.id}")

except Exception as e:
logger.error(f"License deactivation failed: {str(e)}", exc_info=True)

License Key Generation Service

Create licenses/services.py:

"""
License generation and management services.
"""

import secrets
import string
from datetime import datetime
from licenses.models import License


class LicenseService:
"""Service class for license operations."""

# Character set for license keys (excludes ambiguous: 0, O, I, 1)
KEY_CHARSET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'

@classmethod
def generate_unique_key(cls, max_attempts=10):
"""
Generate a unique license key.

Format: CODITECT-{YEAR}-{RANDOM4}-{RANDOM4}
Example: CODITECT-2025-A7B3-X9K2

Returns:
str: Unique license key

Raises:
ValueError: If unable to generate unique key after max_attempts
"""
year = datetime.now().year

for attempt in range(max_attempts):
# Generate two random 4-character segments
segment1 = ''.join(secrets.choice(cls.KEY_CHARSET) for _ in range(4))
segment2 = ''.join(secrets.choice(cls.KEY_CHARSET) for _ in range(4))

# Construct key
key = f"CODITECT-{year}-{segment1}-{segment2}"

# Check uniqueness
if not License.objects.filter(key_string=key).exists():
return key

raise ValueError(f"Failed to generate unique license key after {max_attempts} attempts")

@classmethod
def validate_key_format(cls, key_string):
"""
Validate license key format.

Args:
key_string: License key to validate

Returns:
bool: True if format is valid
"""
import re

# Pattern: CODITECT-YYYY-XXXX-XXXX
pattern = r'^CODITECT-\d{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'

return bool(re.match(pattern, key_string))

@classmethod
def sign_license_payload(cls, license):
"""
Sign license payload with Cloud KMS.

Args:
license: License model instance

Returns:
dict: Signed license payload
"""
from api.v1.views.license import sign_license_with_kms

payload = {
'license_id': str(license.id),
'license_key': license.key_string,
'organization_id': str(license.organization.id),
'organization_name': license.organization.name,
'tier': license.tier,
'features': license.features,
'expiry_date': license.expiry_date.isoformat(),
'issued_at': license.created_at.isoformat(),
'is_active': license.is_active,
}

signature = sign_license_with_kms(payload)

return {
'payload': payload,
'signature': signature,
'algorithm': 'RS256',
'key_id': getattr(settings, 'CLOUD_KMS_KEY_NAME', None),
}

Implementation Checklist

Phase 1 Step 3 Tasks

  • Database Migration: Add Stripe integration fields to License model
  • Database Migration: Add active_license field to Organization model
  • Create Service: Implement licenses/services.py with key generation
  • Update Webhook Handler: Extend SubscriptionWebhookView._handle_checkout_completed() to generate license
  • Update Webhook Handler: Extend _handle_subscription_updated() to update license expiry
  • Update Webhook Handler: Extend _handle_subscription_deleted() to deactivate license
  • Create Serializers: LicenseSerializer, LicenseDetailSerializer, LicenseValidateSerializer
  • Create Views: LicenseListView, LicenseDetailView, LicenseValidateView, LicenseDownloadView
  • Update URLs: Add license management endpoints
  • Create Tests: Unit tests for license key generation
  • Create Tests: Integration tests for webhook → license generation
  • Documentation: Update API documentation with new endpoints

Success Criteria

  1. Automatic Generation: License created within 5 seconds of successful Stripe payment
  2. Unique Keys: 100% uniqueness guarantee with cryptographically secure random generation
  3. Subscription Integration: License expiry updates automatically with subscription renewal
  4. Deactivation: License deactivated immediately on subscription cancellation
  5. API Endpoints: All license management endpoints working with proper authentication
  6. Audit Logging: All license operations logged for compliance

Timeline Estimate

  • Database Migrations: 30 minutes
  • License Service: 1 hour
  • Webhook Integration: 2 hours
  • API Endpoints: 2 hours
  • Testing: 2 hours
  • Documentation: 30 minutes

Total: 8 hours


Security Considerations

  1. License Key Security:

    • Keys are cryptographically random (not sequential or predictable)
    • No sensitive data encoded in keys
    • Validation requires database lookup (keys cannot be guessed)
  2. API Security:

    • License endpoints require JWT authentication
    • Multi-tenant isolation via django-multitenant
    • License validation endpoint is public but rate-limited
  3. Cloud KMS Signing:

    • License payloads signed with RSA-4096
    • Signatures verify license authenticity
    • Cannot be forged without access to KMS key
  4. Audit Logging:

    • All license operations logged
    • Immutable audit trail for compliance

Next Steps (Phase 1 Step 4)

After license generation is complete:

  1. License Delivery API - Download endpoint for customers
  2. SendGrid Integration (Phase 1 Step 5) - Email license to customers
  3. License Validation Endpoint - For offline license verification
  4. License Management UI (Phase 2) - Customer dashboard for license management

Status: Ready for Implementation Dependencies: Phase 1 Step 2 (Stripe Integration) - ✅ Complete