Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
FeaturesTrial ExperienceImplementation Guide

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:

  1. Read the Technical Architecture for database schema
  2. Review the AI Integration for logging patterns
  3. Ensure access to Supabase dashboard for migrations
  4. Have admin-app access for AI feature registration

Implementation Order

Agent 1: Database & Schema

Priority: Must complete first Duration: ~30 min

Tasks

  1. Create migration file for trial_sessions table
  2. Add trial columns to existing tables
  3. Register AI features in ai_features table
  4. 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.tsx

TrialContext.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.ts

Key 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

  1. Onboarding Layout - Add story generation progress indicator
  2. Book Creation Step - Show “Your first story will be added automatically”
  3. 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