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
- Automatic License Generation: Create licenses immediately after successful Stripe payment
- License Key Format: Generate unique, secure license keys (format:
CODITECT-XXXX-XXXX-XXXX) - Subscription Integration: Link licenses to Stripe subscriptions for automatic expiry updates
- License Management API: Provide endpoints for customers to view and manage their licenses
- 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 Plan | License Tier | Features |
|---|---|---|
| Free | N/A | No license needed (free tier) |
| Pro | PRO | Marketplace, Analytics, Priority Support |
| Enterprise | ENTERPRISE | All 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.pywith 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
- ✅ Automatic Generation: License created within 5 seconds of successful Stripe payment
- ✅ Unique Keys: 100% uniqueness guarantee with cryptographically secure random generation
- ✅ Subscription Integration: License expiry updates automatically with subscription renewal
- ✅ Deactivation: License deactivated immediately on subscription cancellation
- ✅ API Endpoints: All license management endpoints working with proper authentication
- ✅ 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
-
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)
-
API Security:
- License endpoints require JWT authentication
- Multi-tenant isolation via django-multitenant
- License validation endpoint is public but rate-limited
-
Cloud KMS Signing:
- License payloads signed with RSA-4096
- Signatures verify license authenticity
- Cannot be forged without access to KMS key
-
Audit Logging:
- All license operations logged
- Immutable audit trail for compliance
Next Steps (Phase 1 Step 4)
After license generation is complete:
- License Delivery API - Download endpoint for customers
- SendGrid Integration (Phase 1 Step 5) - Email license to customers
- License Validation Endpoint - For offline license verification
- License Management UI (Phase 2) - Customer dashboard for license management
Status: Ready for Implementation Dependencies: Phase 1 Step 2 (Stripe Integration) - ✅ Complete