ADR-014: Trial License Implementation
Status: Accepted Date: 2025-11-30 Deciders: Product Team, Growth Team, Engineering Team Tags: trial, freemium, conversion, growth, product-led-growth
Context
The Freemium → Paid Conversion Challenge
CODITECT offers a Free tier with limited features. However, users need to experience Pro features before committing to $58/month to understand the value proposition.
Current Free Tier Limitations:
- 5 agents (vs. 52 agents in Pro)
- 10 commands (vs. 81 commands in Pro)
- 1 project only (vs. unlimited projects in Pro)
- 24-hour offline grace (vs. 72 hours in Pro)
Problem: Users can't evaluate Pro features without paying first.
Business Impact:
- Low Conversion Rate: <5% of Free users convert to Pro (industry average: 2-5%)
- High Churn Risk: Users who don't experience value churn after 30 days
- Support Overhead: "Can I try Pro?" requests require manual trial setup
Real-World User Journey (Without Trial)
Day 1: Sign up for Free tier
→ Try 5 agents, 10 commands
→ "Hmm, this is useful but limited"
Day 7: Want to use advanced agents (security-specialist, cloud-architect)
→ Blocked by Free tier limit
→ Consider upgrading but hesitant ($58/month commitment)
Day 14: Still on fence about Pro
→ Can't justify $58/month without trying it first
Day 30: Churns to competitor offering free trial
→ Lost customer who might have converted with trial
Industry Benchmarks (SaaS Trials)
| Trial Duration | Conversion Rate | Time to Decision | Optimal For |
|---|---|---|---|
| 7 days | 15-20% | Quick decision | Low-friction products |
| 14 days | 25-30% | Balanced | Developer tools ✅ |
| 30 days | 20-25% | Extended evaluation | Enterprise products |
| No trial | 2-5% | Immediate commitment | High-value products |
Recommendation: 14-day trial balances conversion rate with decision time for developer tools.
Business Requirements
Conversion Goals:
- Primary: Increase Free → Pro conversion from 5% to 20%
- Secondary: Reduce trial → paid churn from 40% to 15%
- Metric: Trial conversion rate >25%
Trial Experience:
- Full Pro features (52 agents, 81 commands, unlimited projects)
- No credit card required upfront (reduce friction)
- Clear expiration countdown (create urgency)
- Seamless upgrade path (1-click to paid)
Anti-Abuse:
- Limit to 1 trial per email address
- Block temporary/disposable emails
- Track trial abuse patterns (multiple accounts)
Notification Strategy:
- Day 0: Trial started email (welcome, value proposition)
- Day 7: Mid-trial email (success stories, testimonials)
- Day 12: 2 days remaining email (urgency, upgrade CTA)
- Day 14: Trial expired email (limited time upgrade offer)
Decision
We will implement 14-day Pro trials with:
- No Credit Card Required (reduce signup friction)
- Full Pro Feature Access (52 agents, 81 commands, unlimited projects)
- Automatic Downgrade to Free tier after 14 days (no hard cutoff)
- Email Nurture Sequence (Day 0, 7, 12, 14)
- 1-Click Upgrade during trial (seamless conversion)
- Trial Conversion Tracking (analytics on conversion funnel)
Trial Lifecycle Architecture
┌────────────────────────────────────────────────────────────────┐
│ Trial License Lifecycle │
└────────────────────────────────────────────────────────────────┘
Free Tier User
│
│ Click "Start Pro Trial"
▼
┌───────────────────────┐
│ POST /api/v1/trials │
│ {user_id} │
└───────┬───────────────┘
│
│ Validation:
│ - Email not disposable?
│ - No previous trial?
│ - Account >24 hours old?
▼
┌───────────────────────┐
│ Create Trial License │
│ │
│ status: trial │
│ trial_tier: pro │
│ trial_expires_at: │
│ now + 14 days │
│ original_tier: free │
└───────┬───────────────┘
│
│ Feature gates updated ✅
│ Pro features unlocked immediately
▼
┌───────────────────────┐
│ Day 0: Trial Started │
│ │
│ 📧 Welcome Email │
│ - Trial duration │
│ - Pro features │
│ - Value proposition │
└───────┬───────────────┘
│
▼
Day 1-6: Active Trial (Normal Usage)
│
▼
┌───────────────────────┐
│ Day 7: Mid-Trial │
│ │
│ 📧 Success Stories │
│ - Customer wins │
│ - Use case examples │
│ - Upgrade CTA │
└───────┬───────────────┘
│
▼
Day 8-11: Active Trial (Continued Usage)
│
▼
┌───────────────────────┐
│ Day 12: 2 Days Left │
│ │
│ 📧 Urgency Email │
│ - Trial ending soon │
│ - Discount offer │
│ - 1-click upgrade │
└───────┬───────────────┘
│
▼
Day 13: Last Day (Final Chance)
│
▼
┌───────────────────────┐
│ Day 14: Trial Expired│
├───────────────────────┤
│ │
│ User Converted? │
│ ├─► YES │
│ │ └─► Pro License │
│ │ │
│ └─► NO │
│ └─► Downgrade │
│ to Free │
└───────┬───────────────┘
│
├─► Converted ✅
│ │
│ │ Stripe subscription created
│ │ License upgraded to Pro
│ │ 📧 Welcome to Pro email
│ │
│ └─► Pro features continue
│
└─► Not Converted ❌
│
│ Downgrade to Free tier
│ Feature gates revert
│ 📧 Limited-time upgrade offer
│
└─► Free features only
Celery Task: Daily Trial Check
┌─────────────────────────────────────────────────────────────┐
│ Celery Beat: check_trial_expirations │
│ Schedule: Daily at 00:10 UTC │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Get All Trials │
│ Status: trial │
└───────┬───────────────┘
│
▼
┌───────────────────────┐
│ For Each Trial: │
│ │
│ days_remaining = │
│ trial_expires_at │
│ - now │
└───────┬───────────────┘
│
▼
┌────────────┴─────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ days == 7? │ │ days == 2? │
│ Send email │ │ Send email │
│ (success) │ │ (urgency) │
└──────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ days == 1? │ │ days == 0? │
│ Send email │ │ Downgrade │
│ (last day) │ │ to Free │
└──────────────┘ └──────┬───────┘
│
┌──────┴───────┐
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Converted? │ │ Not converted│
│ → keep Pro │ │ → downgrade │
└─────────────┘ └──────────────┘
Implementation
1. Database Schema
File: backend/licenses/models.py
from django.db import models
from django.utils import timezone
from datetime import timedelta
import uuid
class License(models.Model):
"""License model with trial support."""
class Status(models.TextChoices):
ACTIVE = 'active', 'Active'
TRIAL = 'trial', 'Trial'
GRACE = 'grace', 'Grace Period'
SUSPENDED = 'suspended', 'Suspended'
CANCELLED = 'cancelled', 'Cancelled'
# ... existing fields ...
# Trial-specific fields
is_trial = models.BooleanField(default=False)
trial_tier = models.ForeignKey(
'Tier',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='trial_licenses',
help_text="Tier being trialed (e.g., Pro)"
)
trial_started_at = models.DateTimeField(null=True, blank=True)
trial_expires_at = models.DateTimeField(null=True, blank=True)
trial_converted_at = models.DateTimeField(null=True, blank=True)
trial_converted = models.BooleanField(default=False)
# Trial notification tracking
trial_welcome_sent = models.BooleanField(default=False)
trial_midpoint_sent = models.BooleanField(default=False)
trial_ending_sent = models.BooleanField(default=False)
trial_expired_sent = models.BooleanField(default=False)
# Original tier (for downgrade after trial)
original_tier = models.ForeignKey(
'Tier',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='original_licenses'
)
@property
def trial_days_remaining(self) -> int:
"""Calculate days remaining in trial."""
if not self.is_trial or not self.trial_expires_at:
return 0
delta = self.trial_expires_at - timezone.now()
return max(0, delta.days)
@property
def trial_expired(self) -> bool:
"""Check if trial has expired."""
return (
self.is_trial and
self.trial_expires_at and
timezone.now() > self.trial_expires_at
)
def start_trial(self, trial_tier, duration_days=14) -> None:
"""
Start trial period for specified tier.
Args:
trial_tier: Tier to trial (e.g., Pro)
duration_days: Trial duration (default: 14 days)
"""
self.is_trial = True
self.status = self.Status.TRIAL
self.original_tier = self.tier # Save current tier (Free)
self.tier = trial_tier # Upgrade to trial tier temporarily
self.trial_tier = trial_tier
self.trial_started_at = timezone.now()
self.trial_expires_at = timezone.now() + timedelta(days=duration_days)
# Reset notification flags
self.trial_welcome_sent = False
self.trial_midpoint_sent = False
self.trial_ending_sent = False
self.trial_expired_sent = False
self.save()
def convert_trial(self, stripe_subscription_id: str) -> None:
"""
Convert trial to paid subscription.
Args:
stripe_subscription_id: Stripe subscription ID
"""
self.is_trial = False
self.trial_converted = True
self.trial_converted_at = timezone.now()
self.status = self.Status.ACTIVE
self.stripe_subscription_id = stripe_subscription_id
self.auto_renew = True
# Keep trial tier as permanent tier
self.original_tier = None
# Set expiration based on subscription
self.expires_at = timezone.now() + timedelta(days=30)
self.save()
def end_trial(self) -> None:
"""End trial and downgrade to original tier."""
if not self.is_trial:
return
# Revert to original tier (Free)
self.tier = self.original_tier if self.original_tier else self.tier
self.is_trial = False
self.status = self.Status.ACTIVE
self.trial_tier = None
self.save()
class TrialRequest(models.Model):
"""
Track trial requests for anti-abuse.
Prevents users from creating multiple trials with same email or IP.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey('Tenant', on_delete=models.CASCADE)
license = models.ForeignKey('License', on_delete=models.SET_NULL, null=True)
email = models.EmailField(db_index=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
requested_at = models.DateTimeField(auto_now_add=True)
approved = models.BooleanField(default=True)
rejection_reason = models.CharField(max_length=255, blank=True)
class Meta:
db_table = 'trial_requests'
ordering = ['-requested_at']
indexes = [
models.Index(fields=['email', '-requested_at']),
models.Index(fields=['ip_address', '-requested_at']),
]
def __str__(self):
return f"Trial Request: {self.email} at {self.requested_at}"
2. Trial Activation Endpoint
File: backend/licenses/views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
import re
from .models import License, TrialRequest, Tier
from .notifications import send_trial_welcome_email
DISPOSABLE_EMAIL_DOMAINS = [
'mailinator.com', 'guerrillamail.com', 'temp-mail.org',
'10minutemail.com', 'throwaway.email', 'tempmail.com'
]
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def start_trial(request):
"""
Start 14-day Pro trial.
Request Body:
{
"trial_tier": "pro"
}
Response:
{
"trial_started": true,
"trial_expires_at": "2025-12-14T00:00:00Z",
"days_remaining": 14,
"trial_tier": "pro"
}
Validation:
- No previous trial for this email
- Email not disposable
- Account >24 hours old (anti-abuse)
- Free tier only (Pro/Team users can't trial)
Returns:
200: Trial started successfully
400: Invalid request or trial not allowed
429: Trial limit exceeded
"""
tenant = request.user.tenant
trial_tier_slug = request.data.get('trial_tier', 'pro')
# Validation 1: Check if already on trial
if tenant.licenses.filter(is_trial=True).exists():
return Response(
{'error': 'Trial already active'},
status=status.HTTP_400_BAD_REQUEST
)
# Validation 2: Check if already had trial
if TrialRequest.objects.filter(email=tenant.billing_email).exists():
return Response(
{'error': 'Trial already used for this email'},
status=status.HTTP_429_TOO_MANY_REQUESTS
)
# Validation 3: Check disposable email
email_domain = tenant.billing_email.split('@')[1]
if email_domain in DISPOSABLE_EMAIL_DOMAINS:
# Log rejection
TrialRequest.objects.create(
tenant=tenant,
email=tenant.billing_email,
ip_address=get_client_ip(request),
approved=False,
rejection_reason='disposable_email'
)
return Response(
{'error': 'Disposable email addresses not allowed for trials'},
status=status.HTTP_400_BAD_REQUEST
)
# Validation 4: Account age (>24 hours)
account_age = timezone.now() - tenant.created_at
if account_age < timedelta(hours=24):
return Response(
{'error': 'Account must be at least 24 hours old to start trial'},
status=status.HTTP_400_BAD_REQUEST
)
# Validation 5: Must be on Free tier
current_license = tenant.licenses.filter(status=License.Status.ACTIVE).first()
if not current_license:
return Response(
{'error': 'No active license found'},
status=status.HTTP_400_BAD_REQUEST
)
if current_license.tier.slug != 'free':
return Response(
{'error': 'Trials only available for Free tier users'},
status=status.HTTP_400_BAD_REQUEST
)
# Get trial tier
try:
trial_tier = Tier.objects.get(slug=trial_tier_slug)
except Tier.DoesNotExist:
return Response(
{'error': f"Invalid trial tier: {trial_tier_slug}"},
status=status.HTTP_400_BAD_REQUEST
)
# Start trial
current_license.start_trial(trial_tier, duration_days=14)
# Record trial request
trial_request = TrialRequest.objects.create(
tenant=tenant,
license=current_license,
email=tenant.billing_email,
ip_address=get_client_ip(request),
approved=True
)
# Send welcome email
send_trial_welcome_email(current_license)
# Record event
LicenseEvent.objects.create(
license=current_license,
tenant=tenant,
event_type='trial_started',
metadata={
'trial_tier': trial_tier_slug,
'trial_duration_days': 14,
'trial_expires_at': current_license.trial_expires_at.isoformat()
}
)
logger.info(
f"Trial started",
extra={
'license_key': current_license.license_key,
'trial_tier': trial_tier_slug,
'email': tenant.billing_email
}
)
return Response({
'trial_started': True,
'trial_expires_at': current_license.trial_expires_at,
'days_remaining': current_license.trial_days_remaining,
'trial_tier': trial_tier_slug
}, status=status.HTTP_200_OK)
def get_client_ip(request):
"""Extract client IP from request headers."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
3. Celery Task: Trial Expiration Check
File: backend/licenses/tasks.py
from celery import shared_task
from django.utils import timezone
import logging
from .models import License, LicenseEvent
from .notifications import (
send_trial_midpoint_email,
send_trial_ending_email,
send_trial_expired_email
)
logger = logging.getLogger(__name__)
@shared_task(
bind=True,
name='licenses.tasks.check_trial_expirations',
soft_time_limit=300,
time_limit=360
)
def check_trial_expirations(self):
"""
Daily task to check trial expirations and send notifications.
Schedule: Daily at 00:10 UTC
Workflow:
1. Get all active trials
2. For each trial:
- Day 7: Send midpoint email
- Day 12: Send ending soon email
- Day 13: Send last day email
- Day 14: Downgrade to Free tier
3. Record conversion analytics
Returns:
Dict with processing statistics
"""
try:
logger.info("Starting trial expiration check")
stats = {
'trials_checked': 0,
'midpoint_emails_sent': 0,
'ending_emails_sent': 0,
'trials_expired': 0,
'trials_downgraded': 0
}
# Get all active trials
trials = License.objects.filter(
is_trial=True,
status=License.Status.TRIAL
).select_related('tenant', 'tier', 'trial_tier')
now = timezone.now()
for license_obj in trials:
stats['trials_checked'] += 1
try:
days_remaining = license_obj.trial_days_remaining
# Day 7 (midpoint) - Success stories
if days_remaining == 7 and not license_obj.trial_midpoint_sent:
send_trial_midpoint_email(license_obj)
license_obj.trial_midpoint_sent = True
license_obj.save()
stats['midpoint_emails_sent'] += 1
logger.info(
f"Sent trial midpoint email",
extra={'license_key': license_obj.license_key}
)
# Day 12 (2 days left) - Urgency + discount
elif days_remaining == 2 and not license_obj.trial_ending_sent:
send_trial_ending_email(license_obj)
license_obj.trial_ending_sent = True
license_obj.save()
stats['ending_emails_sent'] += 1
logger.info(
f"Sent trial ending email",
extra={'license_key': license_obj.license_key}
)
# Day 14 (expired) - Downgrade or convert
elif days_remaining == 0:
stats['trials_expired'] += 1
# Check if user converted during trial
if license_obj.trial_converted:
logger.info(
f"Trial converted to paid",
extra={'license_key': license_obj.license_key}
)
# Already converted, no action needed
continue
# Not converted - downgrade to Free
original_tier = license_obj.original_tier
license_obj.end_trial()
send_trial_expired_email(license_obj)
# Record event
LicenseEvent.objects.create(
license=license_obj,
tenant=license_obj.tenant,
event_type='trial_expired',
metadata={
'trial_tier': license_obj.trial_tier.slug if license_obj.trial_tier else None,
'original_tier': original_tier.slug if original_tier else 'free',
'trial_duration': 14,
'converted': False
}
)
stats['trials_downgraded'] += 1
logger.info(
f"Trial expired, downgraded to Free",
extra={'license_key': license_obj.license_key}
)
except Exception as e:
logger.error(
f"Error processing trial",
extra={
'license_key': license_obj.license_key,
'error': str(e)
},
exc_info=True
)
continue
logger.info(
f"Trial expiration check completed",
extra=stats
)
return stats
except Exception as e:
logger.error(
f"Unexpected error in trial expiration check",
extra={'error': str(e)},
exc_info=True
)
return {'error': str(e)}
4. Email Notifications
File: backend/licenses/notifications.py
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.conf import settings
from .models import License
def send_trial_welcome_email(license_obj: License):
"""Send trial welcome email."""
subject = "Your 14-Day CODITECT Pro Trial Has Started!"
context = {
'license_key': license_obj.license_key,
'trial_expires_at': license_obj.trial_expires_at,
'days_remaining': 14,
'trial_tier': license_obj.trial_tier.name,
'features': [
'52 AI agents',
'81 slash commands',
'Unlimited projects',
'72-hour offline mode',
'Priority support'
]
}
html_message = render_to_string('emails/trial_welcome.html', context)
plain_message = render_to_string('emails/trial_welcome.txt', context)
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[license_obj.tenant.billing_email],
html_message=html_message
)
def send_trial_midpoint_email(license_obj: License):
"""Send trial midpoint email (Day 7)."""
subject = "You're Halfway Through Your CODITECT Pro Trial"
context = {
'license_key': license_obj.license_key,
'days_remaining': license_obj.trial_days_remaining,
'upgrade_url': f"{settings.FRONTEND_URL}/billing/upgrade",
'success_stories': [
{
'customer': 'Jane Smith, Senior Engineer',
'quote': 'CODITECT cut my development time by 40%',
'company': 'TechCorp'
},
# ... more stories
]
}
html_message = render_to_string('emails/trial_midpoint.html', context)
send_mail(
subject=subject,
message=render_to_string('emails/trial_midpoint.txt', context),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[license_obj.tenant.billing_email],
html_message=html_message
)
def send_trial_ending_email(license_obj: License):
"""Send trial ending email (Day 12 - 2 days left)."""
subject = "Your CODITECT Pro Trial Ends in 2 Days - Upgrade Now!"
context = {
'license_key': license_obj.license_key,
'days_remaining': license_obj.trial_days_remaining,
'upgrade_url': f"{settings.FRONTEND_URL}/billing/upgrade?discount=TRIAL20",
'discount_code': 'TRIAL20', # 20% off first month
'discount_amount': '$11.60' # 20% of $58
}
html_message = render_to_string('emails/trial_ending.html', context)
send_mail(
subject=subject,
message=render_to_string('emails/trial_ending.txt', context),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[license_obj.tenant.billing_email],
html_message=html_message
)
def send_trial_expired_email(license_obj: License):
"""Send trial expired email (Day 14)."""
subject = "Your CODITECT Pro Trial Has Ended"
context = {
'license_key': license_obj.license_key,
'upgrade_url': f"{settings.FRONTEND_URL}/billing/upgrade?discount=LAST_CHANCE30",
'discount_code': 'LAST_CHANCE30', # 30% off first month
'discount_amount': '$17.40' # 30% of $58
}
html_message = render_to_string('emails/trial_expired.html', context)
send_mail(
subject=subject,
message=render_to_string('emails/trial_expired.txt', context),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[license_obj.tenant.billing_email],
html_message=html_message
)
5. Analytics Tracking
File: backend/analytics/trial_conversion.py
import logging
from django.db.models import Count, Q, F
from django.utils import timezone
from datetime import timedelta
from licenses.models import License, TrialRequest
logger = logging.getLogger(__name__)
class TrialConversionAnalytics:
"""
Track trial conversion metrics for growth analytics.
"""
def get_trial_stats(self, days=30):
"""
Get trial conversion statistics for last N days.
Returns:
{
'trials_started': 100,
'trials_converted': 25,
'trials_expired': 50,
'trials_active': 25,
'conversion_rate': 0.25,
'revenue_generated': 1450.00
}
"""
cutoff = timezone.now() - timedelta(days=days)
# Trials started
trials_started = TrialRequest.objects.filter(
requested_at__gte=cutoff,
approved=True
).count()
# Trials converted
trials_converted = License.objects.filter(
trial_started_at__gte=cutoff,
trial_converted=True
).count()
# Trials expired (not converted)
trials_expired = License.objects.filter(
trial_started_at__gte=cutoff,
is_trial=False,
trial_converted=False,
trial_expires_at__lt=timezone.now()
).count()
# Trials active (ongoing)
trials_active = License.objects.filter(
is_trial=True,
status=License.Status.TRIAL,
trial_expires_at__gte=timezone.now()
).count()
# Conversion rate
conversion_rate = trials_converted / trials_started if trials_started > 0 else 0
# Revenue generated from trials
revenue = trials_converted * 58.00 # Pro monthly price
return {
'trials_started': trials_started,
'trials_converted': trials_converted,
'trials_expired': trials_expired,
'trials_active': trials_active,
'conversion_rate': conversion_rate,
'revenue_generated': revenue
}
def get_conversion_funnel(self):
"""
Get trial conversion funnel.
Returns:
{
'trial_started': 100,
'day_7_active': 80, # 80% still active on day 7
'day_14_active': 60, # 60% still active on day 14
'converted': 25, # 25% converted
'churned': 75 # 75% churned
}
"""
# Implementation for funnel analysis
pass
Consequences
Positive
✅ Higher Conversion Rate
- Target: 25% trial conversion (vs. 5% without trial)
- 5x improvement in Free → Pro conversion
- $1,450 additional MRR per 100 trial starts
✅ Reduced Signup Friction
- No credit card required
- 1-click trial activation
- Immediate Pro feature access
✅ Try-Before-Buy Experience
- Users experience full value before commitment
- Reduced purchase anxiety
- Lower post-purchase churn
✅ Automated Nurture Sequence
- Day 0/7/12/14 emails guide users to value
- Success stories build confidence
- Urgency emails create FOMO
✅ Anti-Abuse Protection
- Email validation (no disposable emails)
- 1 trial per email lifetime
- Account age requirement (24 hours)
Negative
⚠️ Abuse Risk
- Users create multiple accounts with different emails
- Mitigation: IP tracking, device fingerprinting
- Enforcement: Manual review of suspicious patterns
⚠️ Revenue Delay
- 14-day delay before first payment
- Mitigation: Focus on LTV, not immediate revenue
- Alternative: 7-day trial for lower friction
⚠️ Support Overhead
- Trial users generate support tickets
- Mitigation: Self-service docs, automated answers
- Cost: ~5% of trials generate tickets
Neutral
🔄 Email Frequency
- 4 emails over 14 days
- Some users may find it excessive
- Mitigation: Unsubscribe option, preference center
Testing Strategy
Test Scenarios
-
Trial Activation
- Free user starts trial
- Verify status = trial
- Verify Pro features unlocked
-
Trial Conversion
- User upgrades during trial
- Verify Stripe subscription created
- Verify trial_converted = true
-
Trial Expiration
- Trial expires without conversion
- Verify downgrade to Free
- Verify email sent
-
Anti-Abuse
- Attempt trial with disposable email
- Attempt second trial with same email
- Verify rejections logged
Related ADRs
- ADR-012: License Expiration and Renewal
- ADR-013: Stripe Integration for Billing
- ADR-010: Feature Gating Matrix (trial feature access)
References
Last Updated: 2025-11-30 Owner: Growth Team Review Cycle: Monthly (optimize based on conversion data)