ADR-061: Provisioning Strategies - License & Workstation
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-01-09 |
| Decision Makers | Hal Casteel (CEO/CTO), Architecture Team |
| Supersedes | N/A |
| Related ADRs | ADR-003, ADR-010, ADR-009 |
Executive Summary
CODITECT supports three distinct provisioning strategies for licenses and workstations:
- Paid Flow - Standard commercial path via Stripe checkout
- Admin License Provision - Manual license creation for pilot/trial/comp/internal users
- Admin Direct Workstation - Workstation provisioning independent of license status
Key Decision: Licenses and Workstations are independent entities that can be provisioned separately. This enables flexible onboarding for pilot customers, trials, and internal testing while maintaining a clean commercial flow for paying customers.
Context and Problem Statement
CODITECT needs to support multiple customer acquisition paths:
- Paying Customers - Self-service via Stripe with automatic provisioning
- Pilot Customers - Hand-selected early adopters who need access without payment
- Trial Users - Time-limited evaluation access
- Complimentary Access - Partners, advisors, investors
- Internal Users - AZ1.AI team members for testing/development
Requirements
Functional:
- Admins can provision licenses without Stripe payment
- Admins can provision workstations without requiring a license first
- All provisioning types use the same underlying models and services
- Audit trail for all admin provisioning actions
- Batch provisioning for multiple pilot customers
Non-Functional:
- No code duplication between provisioning paths
- Clear separation of concerns (license vs workstation)
- Extensible for future provisioning types
- Full audit compliance (who, what, when, why)
Decision
We implement three provisioning strategies with a decoupled architecture where licenses and workstations are independent:
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ CODITECT PROVISIONING ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FLOW 1: PAID (Stripe) │ │
│ │ │ │
│ │ User ──► Stripe Checkout ──► Webhook ──► License Service ──► │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ License Created │ │ │
│ │ │ provisioning_type │ │ │
│ │ │ = "paid" │ │ │
│ │ └─────────┬───────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Workstation │ │ │
│ │ │ Auto-Provisioned │ │ │
│ │ │ via Celery Task │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FLOW 2: ADMIN LICENSE PROVISION │ │
│ │ │ │
│ │ Admin ──► POST /api/v1/admin/licenses/provision/ ──► │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ License Created │ │ │
│ │ │ provisioning_type = │ │ │
│ │ │ "pilot" | "trial" | │ │ │
│ │ │ "comp" | "internal" │ │ │
│ │ │ provisioned_by = admin_id │ │ │
│ │ │ provisioning_notes = "..." │ │ │
│ │ └─────────────┬───────────────┘ │ │
│ │ │ │ │
│ │ ▼ (if auto_provision_workstation=true) │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Workstation Provisioned │ │ │
│ │ │ via Celery Task │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FLOW 3: ADMIN DIRECT WORKSTATION │ │
│ │ │ │
│ │ Admin ──► POST /api/v1/admin/workstations/provision/ ──► │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Workstation Created │ │ │
│ │ │ (No license required) │ │ │
│ │ │ tier = standard|power|ai │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ │ │
│ │ Use Cases: │ │
│ │ - Pre-license setup for demos │ │
│ │ - Internal development/testing │ │
│ │ - Troubleshooting user issues │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Provisioning Type Field
License Model Extension
class License(models.Model):
# ... existing fields ...
PROVISIONING_TYPES = [
('paid', 'Paid (Stripe)'), # Standard commercial
('pilot', 'Pilot Customer'), # Early adopter program
('trial', 'Trial'), # Time-limited evaluation
('comp', 'Complimentary'), # Partners, advisors, investors
('internal', 'Internal'), # AZ1.AI team members
]
provisioning_type = models.CharField(
max_length=20,
choices=PROVISIONING_TYPES,
default='paid',
db_index=True,
help_text="How the license was provisioned"
)
provisioned_by = models.ForeignKey(
'users.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='provisioned_licenses',
help_text="Admin who manually provisioned (null for paid)"
)
provisioning_notes = models.TextField(
blank=True,
help_text="Admin notes explaining provisioning reason"
)
Provisioning Type Descriptions
| Type | Description | Duration | Auto-Renew | Use Case |
|---|---|---|---|---|
paid | Standard Stripe purchase | Subscription-based | Yes | Normal customers |
pilot | Pilot program participant | 1 year default | No | Early adopters, beta testers |
trial | Time-limited evaluation | 14-30 days | No | Prospects evaluating product |
comp | Complimentary access | Varies | No | Partners, advisors, investors |
internal | Internal team member | 1 year | No | AZ1.AI employees, contractors |
API Endpoints
Flow 1: Paid (Existing)
POST /api/v1/subscriptions/checkout/
→ Creates Stripe checkout session
→ Webhook creates license with provisioning_type="paid"
→ Auto-provisions workstation via Celery
Flow 2: Admin License Provision
POST /api/v1/admin/licenses/provision/
{
"user_email": "pilot@customer.com",
"tier": "PRO",
"provisioning_type": "pilot",
"duration_days": 365,
"max_containers": 5,
"features": ["marketplace", "analytics"],
"notes": "Pilot customer - contacted via LinkedIn",
"auto_provision_workstation": true,
"workstation_tier": "standard"
}
Response 201:
{
"license_id": "uuid",
"license_key": "CODITECT-XXXX-XXXX-XXXX",
"user_email": "pilot@customer.com",
"organization_id": "uuid",
"tier": "PRO",
"provisioning_type": "pilot",
"expiry_date": "2027-01-09T00:00:00Z",
"workstation_provisioned": true,
"workstation_id": "uuid"
}
Flow 2B: Batch Provisioning
POST /api/v1/admin/pilot/batch-provision/
{
"customers": [
{"user_email": "pilot1@company.com", "tier": "PRO"},
{"user_email": "pilot2@company.com", "tier": "ENTERPRISE"}
],
"provisioning_type": "pilot",
"duration_days": 365,
"auto_provision_workstations": true
}
Response 200:
{
"total_requested": 2,
"successful": 2,
"failed": 0,
"results": [...]
}
Flow 3: Admin Direct Workstation
POST /api/v1/admin/workstations/provision/
{
"user_email": "demo@customer.com",
"tier": "standard",
"notes": "Demo workstation for sales call",
"auto_start": false
}
Response 201:
{
"workstation_id": "ws-tenant-user",
"user_email": "demo@customer.com",
"organization_id": "uuid",
"tier": "standard",
"status": "CREATING",
"gcp_resource_name": "projects/.../workstations/ws-...",
"estimated_ready_seconds": 300
}
Decoupled Architecture
Key Principle: Independence
Licenses and Workstations are NOT tightly coupled.
| Entity | Identifier | Can Exist Without |
|---|---|---|
| License | organization_id | Workstation (yes) |
| Workstation | tenant_id + user_id | License (yes) |
Why Decoupled?
- Flexibility: Provision workstation before license (demos, testing)
- Simplicity: Each entity has clear ownership and lifecycle
- Resilience: License issues don't block workstation access
- Use Cases:
- Demo: Workstation without license
- API-only: License without workstation
- Full access: Both license and workstation
Audit Trail
All admin provisioning actions are logged to AuditLog:
AuditLog.objects.create(
organization=user.organization,
user=admin_user,
action='LICENSE_PROVISIONED_ADMIN', # or WORKSTATION_PROVISIONED_ADMIN
resource_type='license', # or 'workstation'
resource_id=license_obj.id,
metadata={
'license_key': 'CODITECT-XXXX-...',
'tier': 'PRO',
'provisioning_type': 'pilot',
'duration_days': 365,
'recipient_email': 'pilot@customer.com',
'notes': 'Pilot customer - LinkedIn outreach',
}
)
Audit Actions
| Action | Description |
|---|---|
LICENSE_PROVISIONED_ADMIN | Admin manually provisioned license |
LICENSE_PROVISIONED_ADMIN_BATCH | Batch license provisioning |
LICENSE_EXTENDED_ADMIN | Admin extended license expiry |
LICENSE_REVOKED_ADMIN | Admin revoked license |
WORKSTATION_PROVISIONED_ADMIN | Admin provisioned workstation |
WORKSTATION_DELETED_ADMIN | Admin deleted workstation |
Security Considerations
Authorization
- All admin endpoints require
IsAdminUserpermission - Admin actions are tied to authenticated admin user
- Audit log captures admin identity for compliance
Abuse Prevention
- Rate Limiting: Admin endpoints rate-limited to prevent abuse
- Batch Limits: Max 100 customers per batch provision
- Duration Caps: Max 3650 days (10 years) for manual licenses
- Audit Trail: All actions logged with full metadata
Consequences
Positive
- Pilot Flexibility: Can onboard pilot customers without Stripe integration
- Clear Separation: License and workstation lifecycles are independent
- Full Audit: Complete trail of who provisioned what and why
- Extensible: Easy to add new provisioning types (e.g., "education", "nonprofit")
- Batch Operations: Efficiently onboard multiple pilot customers
Negative
- Admin Responsibility: Admins must manage manual licenses (expiry, revocation)
- Potential Misuse: Admin could provision without proper authorization (mitigated by audit)
- Complexity: Three flows to maintain vs single Stripe flow
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Admin provisions unauthorized licenses | Audit trail + periodic review |
| Pilot licenses never convert to paid | Expiry dates + conversion tracking |
| Orphaned workstations (no license) | Admin workstation list view + cleanup |
Implementation Status
| Component | Status | Location |
|---|---|---|
License provisioning_type field | ✅ Complete | licenses/models.py |
| Admin license provision endpoint | ✅ Complete | api/v1/views/admin.py |
| Admin license extend endpoint | ✅ Complete | api/v1/views/admin.py |
| Admin license revoke endpoint | ✅ Complete | api/v1/views/admin.py |
| Batch provisioning endpoint | ✅ Complete | api/v1/views/admin.py |
| Pilot customers list endpoint | ✅ Complete | api/v1/views/admin.py |
| Admin workstation provision endpoint | ✅ Complete | api/v1/views/admin.py |
| Admin workstation list endpoint | ✅ Complete | api/v1/views/admin.py |
| Admin workstation delete endpoint | ✅ Complete | api/v1/views/admin.py |
| Audit logging | ✅ Complete | licenses/models.py |
| Admin UI | ⏳ Pending | Track A.13.4 |
Related Documentation
- ADR-003: License Validation Strategy (heartbeat, offline support)
- ADR-010: Cloud Workstations Architecture (GCP infrastructure)
- ADR-009: Multi-Tenant SaaS Architecture (tenant isolation)
Decision Outcome
Chosen Option: Three-flow provisioning with decoupled license/workstation architecture.
This approach provides maximum flexibility for customer acquisition while maintaining clean separation of concerns and full audit compliance.
Approved By: Hal Casteel, CEO/CTO Date: 2026-01-09