ADR-007: License Tier Feature Gating
Status: Proposed Date: 2025-11-26 Deciders: Product Team, Engineering Team, CODITECT Integration Team Related ADRs: ADR-001 (Hybrid Architecture), ADR-003 (FastAPI vs Django)
Context
The context intelligence platform must support tiered pricing with feature gates to maximize revenue while providing accessible entry points:
Business Requirements
1. Pricing Tiers (Standalone Mode)
| Tier | Price/Month | Target Users | Key Features |
|---|---|---|---|
| Starter | $49 | 1-10 users | 50K messages, keyword search, 30-day retention |
| Pro | $199 | 10-50 users | 500K messages, semantic search, Git correlation, 1-year retention |
| Enterprise | $999+ | 50+ users | Unlimited, team analytics, SSO, custom retention |
2. CODITECT Integration Mode
- Must integrate with CODITECT's existing license management
- CODITECT already has Starter/Pro/Enterprise tiers
- Context intelligence features should map to CODITECT tiers seamlessly
3. Feature Gate Requirements
- Hard gates: Features completely disabled for lower tiers (e.g., semantic search)
- Soft gates: Features available but with limitations (e.g., 50K messages vs. 500K)
- Upgrade prompts: Show "Upgrade to Pro" when user hits limit
- No degradation: Never remove existing data when downgrading (read-only access)
Current CODITECT License System
# CODITECT's existing license model (Django)
class License(models.Model):
organization = models.OneToOneField('Organization', on_delete=models.CASCADE)
tier = models.CharField(
max_length=20,
choices=[
('starter', 'Starter'),
('pro', 'Pro'),
('enterprise', 'Enterprise')
]
)
valid_until = models.DateTimeField()
max_users = models.IntegerField()
features = models.JSONField(default=dict) # Feature flags
# Example usage
if request.user.organization.license.has_feature('ai_assistant'):
# Enable AI assistant
pass
Decision
We will implement a feature flag system that:
- Reuses CODITECT's license management for CODITECT mode
- Implements parallel license system for standalone mode (SQLAlchemy)
- Uses decorator pattern for API-level feature gates
- Provides database-level quotas for soft limits (message counts, storage)
Architecture
┌─────────────────────────────────────────────────────────────┐
│ License Management Layer │
└───────────────────────┬─────────────────────────────────────┘
↓
┌───────────────┴───────────────┐
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ Standalone Mode │ │ CODITECT Mode │
│ (FastAPI) │ │ (Django) │
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ SQLAlchemy │ │ │ │ Django ORM │ │
│ │ License │ │ │ │ CODITECT │ │
│ │ Model │ │ │ │ License │ │
│ │ │ │ │ │ Model │ │
│ │ - tier │ │ │ │ (Reuse!) │ │
│ │ - features │ │ │ │ │ │
│ │ - limits │ │ │ │ │ │
│ └──────────────┘ │ │ └──────────────┘ │
└──────────────────┘ └──────────────────┘
↓ ↓
└───────────────┬───────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Feature Gate Interface (Framework-Agnostic) │
│ │
│ class FeatureGate: │
│ def has_feature(org_id: UUID, feature: str) -> bool │
│ def get_limit(org_id: UUID, limit: str) -> int │
│ def check_quota(org_id: UUID, resource: str) -> bool │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ API Layer (Decorators) │
│ │
│ @require_feature('semantic_search') │
│ async def semantic_search_endpoint(...): │
│ # Only accessible to Pro/Enterprise │
│ pass │
│ │
│ @check_quota('messages', limit_key='max_messages') │
│ async def create_message_endpoint(...): │
│ # Enforces message quota (50K/500K/unlimited) │
│ pass │
└─────────────────────────────────────────────────────────────┘
Implementation
Step 1: Feature Flags Definition
# core/features/feature_flags.py
from enum import Enum
from typing import Dict
class Tier(Enum):
STARTER = 'starter'
PRO = 'pro'
ENTERPRISE = 'enterprise'
# Feature flag definitions
FEATURE_FLAGS: Dict[str, set[Tier]] = {
# Search features
'keyword_search': {Tier.STARTER, Tier.PRO, Tier.ENTERPRISE},
'semantic_search': {Tier.PRO, Tier.ENTERPRISE},
'hybrid_search': {Tier.PRO, Tier.ENTERPRISE},
# Git features
'git_webhook_ingestion': {Tier.STARTER, Tier.PRO, Tier.ENTERPRISE},
'git_commit_correlation': {Tier.PRO, Tier.ENTERPRISE},
# Analytics
'basic_analytics': {Tier.PRO, Tier.ENTERPRISE},
'team_analytics': {Tier.ENTERPRISE},
'custom_reports': {Tier.ENTERPRISE},
# Administration
'sso_authentication': {Tier.ENTERPRISE},
'audit_logs': {Tier.ENTERPRISE},
'custom_retention': {Tier.ENTERPRISE},
}
# Quota limits
QUOTA_LIMITS: Dict[Tier, Dict[str, int]] = {
Tier.STARTER: {
'max_messages': 50_000,
'max_conversations': 1_000,
'max_users': 10,
'retention_days': 30,
'api_calls_per_minute': 60,
},
Tier.PRO: {
'max_messages': 500_000,
'max_conversations': 10_000,
'max_users': 50,
'retention_days': 365,
'api_calls_per_minute': 300,
},
Tier.ENTERPRISE: {
'max_messages': None, # Unlimited
'max_conversations': None,
'max_users': None,
'retention_days': None, # Custom
'api_calls_per_minute': 1000,
},
}
Step 2: Feature Gate Service (Framework-Agnostic)
# core/services/feature_gate_service.py
from typing import Optional
import uuid
class FeatureGateService:
"""Framework-agnostic feature gating service"""
def __init__(self, license_repo: LicenseRepository):
self.license_repo = license_repo
async def has_feature(
self,
organization_id: uuid.UUID,
feature: str
) -> bool:
"""Check if organization has access to feature"""
license = await self.license_repo.get_by_organization(organization_id)
if not license:
return False # No license = no access
# Check if feature is available for tier
allowed_tiers = FEATURE_FLAGS.get(feature, set())
return license.tier in allowed_tiers
async def get_limit(
self,
organization_id: uuid.UUID,
limit_key: str
) -> Optional[int]:
"""Get quota limit for organization"""
license = await self.license_repo.get_by_organization(organization_id)
if not license:
return 0
tier_limits = QUOTA_LIMITS.get(license.tier, {})
return tier_limits.get(limit_key, 0)
async def check_quota(
self,
organization_id: uuid.UUID,
resource: str
) -> tuple[bool, int, int]:
"""
Check if organization is within quota for resource.
Returns:
(within_quota: bool, current_usage: int, limit: int)
"""
# Get current usage
usage = await self.usage_repo.get_usage(organization_id, resource)
# Get limit
limit = await self.get_limit(organization_id, f'max_{resource}')
if limit is None: # Unlimited (Enterprise)
return (True, usage, float('inf'))
return (usage < limit, usage, limit)
async def increment_usage(
self,
organization_id: uuid.UUID,
resource: str,
amount: int = 1
) -> None:
"""Increment usage counter for resource"""
await self.usage_repo.increment(organization_id, resource, amount)
Step 3: API Decorators (FastAPI)
# standalone/api/decorators.py
from functools import wraps
from fastapi import HTTPException, Depends
def require_feature(feature: str):
"""Decorator: Require specific feature flag"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Extract current_user from kwargs (injected by Depends)
current_user = kwargs.get('current_user')
if not current_user:
raise HTTPException(
status_code=401,
detail="Authentication required"
)
# Check feature access
feature_gate = get_feature_gate_service()
has_access = await feature_gate.has_feature(
organization_id=current_user.organization_id,
feature=feature
)
if not has_access:
# Get organization tier for error message
license = await license_repo.get_by_organization(
current_user.organization_id
)
raise HTTPException(
status_code=403,
detail={
"error": "feature_not_available",
"feature": feature,
"current_tier": license.tier.value,
"required_tiers": [
t.value for t in FEATURE_FLAGS.get(feature, set())
],
"upgrade_url": f"/pricing?upgrade={feature}"
}
)
return await func(*args, **kwargs)
return wrapper
return decorator
def check_quota(resource: str, limit_key: str):
"""Decorator: Check quota before allowing operation"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
current_user = kwargs.get('current_user')
if not current_user:
raise HTTPException(status_code=401)
# Check quota
feature_gate = get_feature_gate_service()
within_quota, usage, limit = await feature_gate.check_quota(
organization_id=current_user.organization_id,
resource=resource
)
if not within_quota:
raise HTTPException(
status_code=429,
detail={
"error": "quota_exceeded",
"resource": resource,
"usage": usage,
"limit": limit,
"upgrade_url": "/pricing?quota_exceeded=true"
}
)
# Execute operation
result = await func(*args, **kwargs)
# Increment usage counter (async, after response)
await feature_gate.increment_usage(
organization_id=current_user.organization_id,
resource=resource
)
return result
return wrapper
return decorator
# Usage in API routes
@router.get("/conversations/semantic-search")
@require_feature('semantic_search') # Pro/Enterprise only
async def semantic_search(
q: str,
current_user = Depends(get_current_user)
):
"""Semantic search (Pro/Enterprise only)"""
# Implementation...
pass
@router.post("/conversations")
@check_quota(resource='conversations', limit_key='max_conversations')
async def create_conversation(
data: ConversationCreate,
current_user = Depends(get_current_user)
):
"""Create conversation (quota-limited)"""
# Implementation...
pass
Step 4: CODITECT Django Integration
# coditect/licenses/hooks.py
from coditect.licenses import register_feature_flags
def register_context_intelligence_features():
"""Register Context Intelligence features with CODITECT license system"""
register_feature_flags({
# Map to CODITECT's existing tiers
'context_intelligence_keyword_search': ['Starter', 'Pro', 'Enterprise'],
'context_intelligence_semantic_search': ['Pro', 'Enterprise'],
'context_intelligence_git_correlation': ['Pro', 'Enterprise'],
'context_intelligence_team_analytics': ['Enterprise'],
'context_intelligence_sso': ['Enterprise'],
})
# coditect/api/views.py
from coditect.licenses.decorators import require_license_feature
class ConversationViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
@require_license_feature('context_intelligence_semantic_search')
def semantic_search(self, request):
"""Semantic search (Pro/Enterprise only in CODITECT)"""
# CODITECT decorator already checked license
# Proceed with search
search_service = get_search_service()
results = await search_service.semantic_search(...)
return Response({"results": results})
Step 5: Usage Tracking (Database)
-- Usage tracking table
CREATE TABLE usage_quotas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
-- Resource type
resource_type TEXT NOT NULL, -- 'messages', 'conversations', 'api_calls'
-- Usage counters
current_usage BIGINT DEFAULT 0,
usage_period_start TIMESTAMPTZ DEFAULT NOW(),
usage_period_end TIMESTAMPTZ,
-- Metadata
last_reset_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(organization_id, resource_type, usage_period_start)
);
-- Index for fast lookups
CREATE INDEX idx_usage_quotas_org_resource ON usage_quotas(organization_id, resource_type);
-- RLS policy
ALTER TABLE usage_quotas ENABLE ROW LEVEL SECURITY;
CREATE POLICY organization_isolation ON usage_quotas
FOR ALL
TO authenticated_users
USING (organization_id = current_setting('app.current_organization_id')::uuid);
-- Function to increment usage (atomic)
CREATE OR REPLACE FUNCTION increment_usage(
org_id UUID,
res_type TEXT,
amount INT DEFAULT 1
)
RETURNS BIGINT AS $$
DECLARE
new_usage BIGINT;
BEGIN
INSERT INTO usage_quotas (organization_id, resource_type, current_usage)
VALUES (org_id, res_type, amount)
ON CONFLICT (organization_id, resource_type, usage_period_start)
DO UPDATE SET
current_usage = usage_quotas.current_usage + amount,
updated_at = NOW()
RETURNING current_usage INTO new_usage;
RETURN new_usage;
END;
$$ LANGUAGE plpgsql;
Consequences
Positive
-
✅ Revenue Optimization
- Tiered pricing maximizes revenue (Starter → Pro = 4x revenue)
- Upgrade prompts when users hit limits (conversion opportunity)
- Feature gates incentivize upgrades ("Unlock semantic search in Pro")
-
✅ CODITECT Integration Reuse
- 90% code reuse with CODITECT's license management
- Same license tiers (Starter/Pro/Enterprise)
- Seamless UI (no duplicate license screens)
-
✅ Flexible Feature Flags
- Easy to add new features (
FEATURE_FLAGS['new_feature'] = {Tier.PRO}) - Easy to adjust limits (
QUOTA_LIMITS[Tier.STARTER]['max_messages'] = 100_000) - A/B test pricing (different limits for different cohorts)
- Easy to add new features (
-
✅ Graceful Degradation
- When downgrading, data is read-only (not deleted)
- Users see "Upgrade to edit" messages
- No data loss on tier changes
-
✅ Atomic Usage Tracking
- PostgreSQL function ensures accurate counters (no race conditions)
- Monthly/yearly reset logic
- Real-time quota checking
Negative
-
⚠️ Dual License System Maintenance
- Complexity: Must maintain SQLAlchemy (standalone) AND Django (CODITECT) implementations
- Mitigation: Abstract interface (
FeatureGateService) hides implementation - Overhead: ~5% additional development time for license changes
-
⚠️ Quota Reset Logic
- Problem: Must reset quotas monthly (messages) vs. persistent (users)
- Example: Starter tier gets 50K messages/month, resets on 1st of month
- Mitigation: Celery cron job runs monthly reset
- Edge case: User reaches limit on last day of month → wait 1 day for reset
-
⚠️ Feature Flag Proliferation
- Risk: 100+ feature flags become hard to manage
- Mitigation: Group features hierarchically (
semantic_search→search_*) - Best practice: Limit to 20-30 top-level flags
-
⚠️ Upgrade Friction
- Problem: Users may churn instead of upgrading
- Mitigation: Generous free tier (50K messages = ~500 conversations)
- Mitigation: Upgrade prompts with ROI messaging ("Save 10 hours/month")
Risks and Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Incorrect Quota Enforcement | Low | High | Unit tests, integration tests, manual QA |
| Quota Counter Drift | Low | Medium | Periodic audit (compare DB count vs. quota) |
| License Sync Issues | Low | Medium | Monitor sync lag, alert if >5 seconds |
| Revenue Loss (Churn) | Medium | High | Optimize free tier, improve upgrade UX |
Alternatives Considered
Alternative 1: No Feature Gates (Single Tier)
Architecture: All features available to all users, charge per-user only
Pros:
- ✅ Simplest implementation (no license logic)
- ✅ Best user experience (no friction)
Cons:
- ❌ Revenue loss: Cannot charge premium for advanced features
- ❌ No upsell path: $49/month maximum (vs. $999/month Enterprise)
- ❌ Example loss: 100 Pro customers × $150/month extra = $180K/year lost
Why Rejected: Tiered pricing is industry standard and significantly increases revenue.
Alternative 2: Hard Limits Only (No Soft Limits)
Architecture: Features are 100% disabled OR 100% enabled (no quotas)
Example: Pro users get unlimited messages (no 500K limit)
Pros:
- ✅ Simpler implementation (no quota tracking)
- ✅ Clearer UX (feature available or not)
Cons:
- ❌ Cost risk: Users could store 100M messages (storage cost spike)
- ❌ Abuse risk: Bad actors could spam API
- ❌ No upgrade triggers: Users never hit limits, no incentive to upgrade
Why Rejected: Soft limits are essential for cost control and upgrade conversion.
Alternative 3: Metered Pricing (Usage-Based)
Architecture: Charge per message, per search, per API call
Example: $0.01 per 1K messages, $0.001 per search
Pros:
- ✅ Fair pricing (pay for what you use)
- ✅ No feature gates (all features available)
- ✅ Scales automatically with usage
Cons:
- ❌ Unpredictable costs: Users don't know monthly bill
- ❌ Complex billing: Must track every operation
- ❌ Market fit: Developers prefer flat pricing (vs. AWS-style metering)
Why Rejected: Flat pricing is preferred by SaaS customers for predictability.
Success Metrics
Revenue Metrics
- Upgrade rate: 20%+ of Starter users upgrade to Pro within 6 months
- Average revenue per user (ARPU): $120/month (mix of Starter/Pro/Enterprise)
- Churn rate: <5% monthly (reasonable limits don't cause churn)
Feature Gate Metrics
- Feature gate accuracy: 100% (no false positives/negatives)
- Quota enforcement: 100% (no users exceed limits)
- Upgrade prompt views: 30%+ users see upgrade prompt
Usage Metrics
- Quota utilization: 60%+ users reach 50-80% of quota (good limit tuning)
- Feature adoption: 80%+ Pro users use semantic search (validates pricing)
Implementation Plan
Phase 1: Feature Flag System (Week 1)
- Define
FEATURE_FLAGSandQUOTA_LIMITS - Implement
FeatureGateService(framework-agnostic) - Write unit tests (100+ test cases)
Phase 2: API Decorators (Week 2)
- Implement
@require_featuredecorator - Implement
@check_quotadecorator - Add to all API endpoints
- Integration tests (FastAPI)
Phase 3: Usage Tracking (Week 3)
- Create
usage_quotastable - Implement
increment_usage()function - Celery task for monthly reset
- Monitoring and alerting
Phase 4: CODITECT Integration (Week 4)
- Register features with CODITECT license system
- Test Django integration
- Verify license sync
- End-to-end testing (both modes)
Phase 5: UI & Upgrade Prompts (Week 5)
- Upgrade prompt UI components
- Pricing page
- Usage dashboard (show quota utilization)
- A/B test upgrade messaging
References
Pricing Strategy:
- SaaS Pricing Models - Flat, tiered, usage-based
- Feature Flag Best Practices
Similar Systems:
- GitHub Copilot: Flat pricing ($10/user/month, no tiers)
- Cursor: Flat pricing ($20/month, no tiers)
- LinearB: Tiered pricing (Free, Team, Enterprise)
Related ADRs:
- ADR-001: Hybrid Architecture (standalone + CODITECT pricing)
- ADR-003: FastAPI vs Django (license system for both modes)
Status: Proposed Review Date: 2025-12-03 Projected ADR Score: 38/40 (A) Complexity: Medium (dual license systems, quota tracking) Owner: Product Team + Engineering Team
Next Steps:
- Approve tiered pricing model (Starter $49, Pro $199, Enterprise $999)
- Define final
FEATURE_FLAGSandQUOTA_LIMITS - Implement
FeatureGateService(Week 1) - Add API decorators (Week 2)
- CODITECT integration testing (Week 4)