Implementation Guide
Step-by-step guide for implementing the Trial Experience feature. This guide follows the sub-agent task breakdown for parallel development.
Prerequisites
Before starting implementation:
- Read the Technical Architecture for database schema
- Review the AI Integration for logging patterns
- Ensure access to Supabase dashboard for migrations
- Have admin-app access for AI feature registration
Implementation Order
Agent 1: Database & Schema
Priority: Must complete first Duration: ~30 min
Tasks
- Create migration file for
trial_sessionstable - Add trial columns to existing tables
- Register AI features in
ai_featurestable - Update RLS policies
Migration File
Create: supabase/migrations/YYYYMMDD_create_trial_sessions.sql
-- Create trial_sessions table
CREATE TABLE trial_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE,
started_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
status TEXT CHECK (status IN ('active', 'expired', 'converted')) DEFAULT 'active',
converted_at TIMESTAMPTZ,
conversation_count INTEGER DEFAULT 0,
recording_count INTEGER DEFAULT 0,
total_audio_seconds INTEGER DEFAULT 0,
story_id UUID, -- Will reference stories table after story is created
ip_address INET,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Add trial columns to ai_conversations
ALTER TABLE ai_conversations
ADD COLUMN IF NOT EXISTS trial_session_id UUID REFERENCES trial_sessions(id),
ADD COLUMN IF NOT EXISTS is_trial BOOLEAN DEFAULT false;
-- Add trial columns to recordings
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS trial_session_id UUID REFERENCES trial_sessions(id),
ADD COLUMN IF NOT EXISTS is_trial BOOLEAN DEFAULT false;
-- Create story_generation_jobs table for async processing
CREATE TABLE story_generation_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trial_session_id UUID REFERENCES trial_sessions(id),
user_id UUID REFERENCES auth.users(id),
status TEXT CHECK (status IN ('pending', 'processing', 'completed', 'failed')) DEFAULT 'pending',
story_id UUID, -- Set when complete
error TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX idx_trial_sessions_user ON trial_sessions(user_id);
CREATE INDEX idx_trial_sessions_status ON trial_sessions(status);
CREATE INDEX idx_trial_sessions_expires ON trial_sessions(expires_at);
CREATE INDEX idx_ai_conversations_trial ON ai_conversations(trial_session_id);
CREATE INDEX idx_recordings_trial ON recordings(trial_session_id);
CREATE INDEX idx_story_jobs_status ON story_generation_jobs(status);
-- RLS Policies
ALTER TABLE trial_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE story_generation_jobs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own trial session" ON trial_sessions
FOR SELECT TO authenticated
USING (user_id = auth.uid());
CREATE POLICY "Users can update own trial session" ON trial_sessions
FOR UPDATE TO authenticated
USING (user_id = auth.uid());
CREATE POLICY "Service can insert trial sessions" ON trial_sessions
FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid());
CREATE POLICY "Users can view own story jobs" ON story_generation_jobs
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Register AI features
INSERT INTO ai_features (name, description, default_model, is_enabled) VALUES
('trial_content_validation', 'Validates trial content quality before signup', 'gpt-4o-mini', true),
('trial_story_generation', 'Converts trial content to formatted story', 'gpt-4o', true),
('trial_conversation', 'AI conversation during trial mode', 'gpt-4o', true)
ON CONFLICT (name) DO NOTHING;Agent 2: Trial API Routes
Priority: After database Duration: ~1-2 hours
Files to Create
apps/web-app/app/api/trial/
├── start/route.ts
├── status/route.ts
├── validate/route.ts
├── convert/route.ts
└── story-status/route.ts/api/trial/start/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const supabase = await createClient()
// Check if user already has an active session
const { data: { user } } = await supabase.auth.getUser()
if (user) {
// Check for existing trial session
const { data: existing } = await supabase
.from('trial_sessions')
.select('*')
.eq('user_id', user.id)
.eq('status', 'active')
.single()
if (existing) {
const timeRemaining = Math.max(0,
Math.floor((new Date(existing.expires_at).getTime() - Date.now()) / 1000)
)
return NextResponse.json({
sessionId: existing.id,
expiresAt: existing.expires_at,
timeRemaining,
userId: user.id,
resumed: true
})
}
}
// Sign in anonymously if no user
let userId = user?.id
if (!userId) {
const { data: authData, error: authError } = await supabase.auth.signInAnonymously()
if (authError) {
return NextResponse.json({ error: authError.message }, { status: 400 })
}
userId = authData.user.id
}
// Create trial session (30 minutes)
const expiresAt = new Date(Date.now() + 30 * 60 * 1000)
const { data: session, error } = await supabase
.from('trial_sessions')
.insert({
user_id: userId,
expires_at: expiresAt.toISOString(),
ip_address: request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip')
})
.select()
.single()
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({
sessionId: session.id,
expiresAt: session.expires_at,
timeRemaining: 1800,
userId
})
} catch (error) {
console.error('Trial start error:', error)
return NextResponse.json({ error: 'Failed to start trial' }, { status: 500 })
}
}Key Implementation Notes
- All API routes must validate the trial session exists and is active
- Use
logAIUsage()for any AI calls (see AI Integration) - Handle rate limiting with appropriate error responses
Agent 3: Trial UI Components
Priority: Can run parallel with Agent 2 Duration: ~2-3 hours
Files to Create
apps/web-app/
├── contexts/
│ └── TrialContext.tsx
├── components/trial/
│ ├── TrialTimerBar.tsx
│ ├── TrialQualityIndicator.tsx
│ ├── TrialSignupModal.tsx
│ ├── TrialModeSelector.tsx
│ └── StoryGenerationStatus.tsx
└── app/trial/
├── page.tsx
├── layout.tsx
├── conversation/page.tsx
└── recording/page.tsxTrialContext.tsx
'use client'
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
interface TrialContextValue {
isTrialMode: boolean
sessionId: string | null
timeRemaining: number
isExpired: boolean
qualityScore: number
isStoryReady: boolean
startTrial: () => Promise<void>
refreshStatus: () => Promise<void>
triggerConversion: () => void
}
const TrialContext = createContext<TrialContextValue | null>(null)
export function TrialProvider({ children }: { children: React.ReactNode }) {
const [sessionId, setSessionId] = useState<string | null>(null)
const [timeRemaining, setTimeRemaining] = useState(0)
const [qualityScore, setQualityScore] = useState(0)
const [isStoryReady, setIsStoryReady] = useState(false)
const [showSignupModal, setShowSignupModal] = useState(false)
const startTrial = useCallback(async () => {
const response = await fetch('/api/trial/start', { method: 'POST' })
const data = await response.json()
if (data.sessionId) {
setSessionId(data.sessionId)
setTimeRemaining(data.timeRemaining)
}
}, [])
const refreshStatus = useCallback(async () => {
if (!sessionId) return
const response = await fetch('/api/trial/status')
const data = await response.json()
setTimeRemaining(data.timeRemaining)
setQualityScore(data.qualityScore)
setIsStoryReady(data.isStoryReady)
}, [sessionId])
// Countdown timer
useEffect(() => {
if (timeRemaining <= 0) return
const timer = setInterval(() => {
setTimeRemaining(t => Math.max(0, t - 1))
}, 1000)
return () => clearInterval(timer)
}, [timeRemaining])
// Refresh status periodically
useEffect(() => {
if (!sessionId) return
const interval = setInterval(refreshStatus, 30000) // Every 30s
return () => clearInterval(interval)
}, [sessionId, refreshStatus])
const triggerConversion = () => setShowSignupModal(true)
return (
<TrialContext.Provider value={{
isTrialMode: !!sessionId,
sessionId,
timeRemaining,
isExpired: timeRemaining === 0,
qualityScore,
isStoryReady,
startTrial,
refreshStatus,
triggerConversion
}}>
{children}
{showSignupModal && (
<TrialSignupModal onClose={() => setShowSignupModal(false)} />
)}
</TrialContext.Provider>
)
}
export const useTrial = () => {
const context = useContext(TrialContext)
if (!context) throw new Error('useTrial must be used within TrialProvider')
return context
}TrialTimerBar.tsx
'use client'
import { useTrial } from '@/contexts/TrialContext'
import { cn } from '@/lib/utils'
export function TrialTimerBar() {
const { timeRemaining, isExpired, isStoryReady, triggerConversion } = useTrial()
const minutes = Math.floor(timeRemaining / 60)
const seconds = timeRemaining % 60
const getTimerStyles = () => {
if (timeRemaining <= 60) return 'bg-red-500 animate-pulse'
if (timeRemaining <= 300) return 'bg-amber-500'
return 'bg-primary'
}
return (
<div className={cn(
'fixed top-0 left-0 right-0 z-50 px-4 py-2',
'flex items-center justify-between',
getTimerStyles()
)}>
<div className="flex items-center gap-2 text-white">
<span className="text-sm font-medium">Trial Mode</span>
<span className="text-lg font-bold tabular-nums">
{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
</span>
</div>
<button
onClick={triggerConversion}
className="px-4 py-1 bg-white text-primary rounded-full font-medium text-sm hover:bg-gray-100 transition-colors"
>
{isStoryReady ? 'Save & Sign Up' : "I'm Done"}
</button>
</div>
)
}Agent 4: Component Modifications
Priority: After Agent 2 APIs Duration: ~1-2 hours
ImmersiveConversation Changes
Add these props to the existing component:
interface ImmersiveConversationProps {
// ... existing props
isTrialMode?: boolean
trialSessionId?: string
onTrialSave?: () => void
}
export function ImmersiveConversation({
isTrialMode = false,
trialSessionId,
onTrialSave,
...props
}: ImmersiveConversationProps) {
// Modify API endpoint for trial mode
const conversationEndpoint = isTrialMode
? '/api/trial/conversations'
: '/api/conversations'
// Modify message sending to include trial_session_id
const sendMessage = async (content: string) => {
const response = await fetch(conversationEndpoint, {
method: 'POST',
body: JSON.stringify({
content,
...(isTrialMode && { trial_session_id: trialSessionId })
})
})
// ... rest of send logic
}
// Modify end chat button for trial mode
const handleEndChat = () => {
if (isTrialMode && onTrialSave) {
onTrialSave()
} else {
// Normal end chat logic
}
}
// ... rest of component
}VoiceRecorder Changes
Similar pattern - add trial props and modify save behavior.
Agent 5: Story Generation Pipeline
Priority: After Agent 2 APIs Duration: ~2-3 hours
Files to Create
apps/web-app/lib/trial/
├── story-generator.ts
└── content-extractor.tsKey Functions
// story-generator.ts
export async function generateStoryFromTrial(jobId: string): Promise<{
success: boolean
storyId?: string
error?: string
}>
// content-extractor.ts
export async function getTrialContent(
supabase: SupabaseClient,
trialSessionId: string
): Promise<{
type: 'voice_recording' | 'ai_conversation'
text: string
durationSeconds?: number
messageCount?: number
}>Agent 6: Onboarding Integration
Priority: After Agents 4 and 5 Duration: ~1-2 hours
Modifications
- Onboarding Layout - Add story generation progress indicator
- Book Creation Step - Show “Your first story will be added automatically”
- Post-Payment Redirect - Check story status and redirect to editor
StoryGenerationStatus Component
export function StoryGenerationStatus() {
const [status, setStatus] = useState<'processing' | 'complete' | 'failed'>('processing')
const [storyId, setStoryId] = useState<string | null>(null)
useEffect(() => {
const pollStatus = async () => {
const response = await fetch('/api/trial/story-status')
const data = await response.json()
setStatus(data.status)
if (data.storyId) setStoryId(data.storyId)
}
const interval = setInterval(pollStatus, 2000)
return () => clearInterval(interval)
}, [])
return (
<div className="flex items-center gap-2 p-4 bg-primary/10 rounded-lg">
{status === 'processing' && (
<>
<Spinner />
<span>Creating your story...</span>
</>
)}
{status === 'complete' && (
<>
<CheckIcon className="text-green-500" />
<span>Your story is ready!</span>
</>
)}
</div>
)
}Agent 7: Marketing Site CTA
Priority: Independent, can run in parallel Duration: ~30 min
Modifications
Add trial CTA to marketing site hero and call-to-action sections:
// apps/marketing-site/src/components/Hero.tsx
<Link
href="https://app.mystoryflow.com/trial"
className="inline-flex items-center px-6 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary-dark transition-colors"
>
Try Free - No Signup Required
</Link>Testing Checklist
Before marking implementation complete:
- Anonymous user can start trial
- Timer counts down correctly
- Time warnings appear at 15m, 5m, 1m
- Quality indicator updates in real-time
- Content validation works for valid and invalid content
- Signup modal appears on “Save” click
- Account upgrade preserves trial data
- Story generation completes successfully
- Post-payment redirects to story editor
- Marketing CTA links to trial page
- All AI calls logged to usage tracker
Next Steps
- Testing Scenarios - Detailed test cases