Skip to main content

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 DurationConversion RateTime to DecisionOptimal For
7 days15-20%Quick decisionLow-friction products
14 days25-30%BalancedDeveloper tools
30 days20-25%Extended evaluationEnterprise products
No trial2-5%Immediate commitmentHigh-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:

  1. No Credit Card Required (reduce signup friction)
  2. Full Pro Feature Access (52 agents, 81 commands, unlimited projects)
  3. Automatic Downgrade to Free tier after 14 days (no hard cutoff)
  4. Email Nurture Sequence (Day 0, 7, 12, 14)
  5. 1-Click Upgrade during trial (seamless conversion)
  6. 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

  1. Trial Activation

    • Free user starts trial
    • Verify status = trial
    • Verify Pro features unlocked
  2. Trial Conversion

    • User upgrades during trial
    • Verify Stripe subscription created
    • Verify trial_converted = true
  3. Trial Expiration

    • Trial expires without conversion
    • Verify downgrade to Free
    • Verify email sent
  4. Anti-Abuse

    • Attempt trial with disposable email
    • Attempt second trial with same email
    • Verify rejections logged

  • 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)