Skip to main content

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)

TierPrice/MonthTarget UsersKey Features
Starter$491-10 users50K messages, keyword search, 30-day retention
Pro$19910-50 users500K messages, semantic search, Git correlation, 1-year retention
Enterprise$999+50+ usersUnlimited, 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:

  1. Reuses CODITECT's license management for CODITECT mode
  2. Implements parallel license system for standalone mode (SQLAlchemy)
  3. Uses decorator pattern for API-level feature gates
  4. 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

  1. ✅ 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")
  2. ✅ CODITECT Integration Reuse

    • 90% code reuse with CODITECT's license management
    • Same license tiers (Starter/Pro/Enterprise)
    • Seamless UI (no duplicate license screens)
  3. ✅ 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)
  4. ✅ Graceful Degradation

    • When downgrading, data is read-only (not deleted)
    • Users see "Upgrade to edit" messages
    • No data loss on tier changes
  5. ✅ Atomic Usage Tracking

    • PostgreSQL function ensures accurate counters (no race conditions)
    • Monthly/yearly reset logic
    • Real-time quota checking

Negative

  1. ⚠️ 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
  2. ⚠️ 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
  3. ⚠️ Feature Flag Proliferation

    • Risk: 100+ feature flags become hard to manage
    • Mitigation: Group features hierarchically (semantic_searchsearch_*)
    • Best practice: Limit to 20-30 top-level flags
  4. ⚠️ 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

RiskLikelihoodImpactMitigation
Incorrect Quota EnforcementLowHighUnit tests, integration tests, manual QA
Quota Counter DriftLowMediumPeriodic audit (compare DB count vs. quota)
License Sync IssuesLowMediumMonitor sync lag, alert if >5 seconds
Revenue Loss (Churn)MediumHighOptimize 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_FLAGS and QUOTA_LIMITS
  • Implement FeatureGateService (framework-agnostic)
  • Write unit tests (100+ test cases)

Phase 2: API Decorators (Week 2)

  • Implement @require_feature decorator
  • Implement @check_quota decorator
  • Add to all API endpoints
  • Integration tests (FastAPI)

Phase 3: Usage Tracking (Week 3)

  • Create usage_quotas table
  • 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:

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:

  1. Approve tiered pricing model (Starter $49, Pro $199, Enterprise $999)
  2. Define final FEATURE_FLAGS and QUOTA_LIMITS
  3. Implement FeatureGateService (Week 1)
  4. Add API decorators (Week 2)
  5. CODITECT integration testing (Week 4)