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

F005 - Document Upload System

Objective

Enable authors to upload manuscripts (PDF, DOCX, TXT) with drag-and-drop interface, progress tracking, and secure storage using MyStoryFlow components in Backblaze B2.

Quick Implementation

Using MyStoryFlow Components

  • FileUploader component from @mystoryflow/ui
  • Progress tracking with @mystoryflow/ui components
  • Backblaze B2 storage integration via @mystoryflow/shared
  • Upload API routes in apps/analyzer-app

New Requirements

  • File type validation (PDF, DOCX, TXT only)
  • Size limit enforcement (10MB max)
  • Manuscript metadata extraction
  • Upload session management

MVP Implementation

1. Database Schema

-- Manuscripts table (in analyzer schema) CREATE TABLE analyzer.manuscripts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES auth.users(id), title VARCHAR(255) NOT NULL, author_name VARCHAR(255), file_name VARCHAR(255) NOT NULL, file_size INTEGER NOT NULL, file_type VARCHAR(50) NOT NULL, storage_path TEXT NOT NULL, word_count INTEGER, status VARCHAR(50) DEFAULT 'uploaded', metadata JSONB DEFAULT '{}', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Upload sessions for progress tracking CREATE TABLE analyzer.upload_sessions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id), manuscript_id UUID REFERENCES manuscripts(id), progress INTEGER DEFAULT 0, status VARCHAR(50) DEFAULT 'uploading', error_message TEXT, created_at TIMESTAMP DEFAULT NOW() ); -- Indexes CREATE INDEX idx_manuscripts_user_id ON manuscripts(user_id); CREATE INDEX idx_manuscripts_status ON manuscripts(status);

2. Enhanced File Uploader Component

// apps/analyzer-app/src/components/manuscript/ManuscriptUploader.tsx 'use client' import { useState } from 'react' import { FileUploader, Progress, Alert } from '@mystoryflow/ui' import { uploadManuscript } from '@/lib/manuscript-upload' import { trackAIUsage } from '@mystoryflow/analytics' const ALLOWED_TYPES = [ 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain' ] const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB export function ManuscriptUploader({ onSuccess }: { onSuccess: (id: string) => void }) { const [uploading, setUploading] = useState(false) const [progress, setProgress] = useState(0) const [error, setError] = useState<string | null>(null) const handleUpload = async (file: File) => { // Validate file type if (!ALLOWED_TYPES.includes(file.type)) { setError('Please upload a PDF, DOCX, or TXT file') return } // Validate file size if (file.size > MAX_FILE_SIZE) { setError('File size must be less than 10MB') return } setUploading(true) setError(null) setProgress(0) try { const manuscriptId = await uploadManuscript(file, { onProgress: (percent) => setProgress(percent) }) onSuccess(manuscriptId) } catch (err) { setError(err instanceof Error ? err.message : 'Upload failed') } finally { setUploading(false) } } return ( <div className="space-y-4"> <FileUploader accept=".pdf,.docx,.txt" onUpload={handleUpload} disabled={uploading} description="Upload your manuscript (PDF, DOCX, or TXT - Max 10MB)" /> {uploading && ( <div className="space-y-2"> <Progress value={progress} /> <p className="text-sm text-muted-foreground"> Uploading manuscript... {progress}% </p> </div> )} {error && ( <Alert variant="error"> {error} </Alert> )} </div> ) }

3. Upload Service

// packages/manuscript-analysis/src/services/upload-service.ts import { getSupabaseBrowserClient } from '@mystoryflow/database' import { uploadToB2 } from '@mystoryflow/shared/storage' interface UploadOptions { onProgress?: (percent: number) => void } export async function uploadManuscript( file: File, options: UploadOptions = {} ): Promise<string> { const supabase = getSupabaseBrowserClient() // Get current user const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error('Not authenticated') // Create manuscript record const { data: manuscript, error: dbError } = await supabase .from('analyzer.manuscripts') .insert({ user_id: user.id, title: file.name.replace(/\.[^/.]+$/, ''), // Remove extension file_name: file.name, file_size: file.size, file_type: file.type, status: 'uploading' }) .select() .single() if (dbError) throw dbError // Upload to Backblaze B2 with progress tracking const storagePath = `manuscripts/${user.id}/${manuscript.id}/${file.name}` // Create upload session await supabase.from('analyzer.upload_sessions').insert({ user_id: user.id, manuscript_id: manuscript.id }) try { // Upload file to Backblaze B2 const { error: uploadError } = await uploadToB2({ bucket: 'story-analyzer', path: storagePath, file, onProgress: (percent) => { options.onProgress?.(percent) // Update session progress supabase.from('analyzer.upload_sessions') .update({ progress: percent }) .eq('manuscript_id', manuscript.id) .then(() => {}) } }) if (uploadError) throw uploadError // Update manuscript with storage path await supabase .from('analyzer.manuscripts') .update({ storage_path: storagePath, status: 'uploaded' }) .eq('id', manuscript.id) // Mark session complete await supabase .from('analyzer.upload_sessions') .update({ status: 'completed', progress: 100 }) .eq('manuscript_id', manuscript.id) // Track storage operation for analytics await trackAIUsage({ userId: user.id, operation: 'manuscript-upload', metadata: { fileSize: file.size, fileType: file.type } }) return manuscript.id } catch (error) { // Mark as failed await supabase .from('analyzer.manuscripts') .update({ status: 'failed' }) .eq('id', manuscript.id) await supabase .from('analyzer.upload_sessions') .update({ status: 'failed', error_message: error instanceof Error ? error.message : 'Unknown error' }) .eq('manuscript_id', manuscript.id) throw error } }

4. API Route

// apps/analyzer-app/src/app/api/manuscripts/upload/route.ts import { NextRequest, NextResponse } from 'next/server' import { withAuth } from '@mystoryflow/auth' export async function POST(req: NextRequest) { const session = await withAuth(req) if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } try { const formData = await req.formData() const file = formData.get('file') as File if (!file) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }) } // File validation is handled in the upload service // This endpoint is mainly for future enhancements return NextResponse.json({ message: 'Use client-side upload for better progress tracking' }) } catch (error) { return NextResponse.json( { error: 'Upload failed' }, { status: 500 } ) } }

5. Backblaze B2 Storage Configuration

// Backblaze B2 bucket configuration // Bucket name: story-analyzer // Access: Private // File structure: manuscripts/{user_id}/{manuscript_id}/{filename} // Environment variables needed: // B2_APPLICATION_KEY_ID // B2_APPLICATION_KEY // B2_BUCKET_NAME=story-analyzer

MVP Acceptance Criteria

  • Drag-and-drop file upload interface
  • Support for PDF, DOCX, TXT formats
  • 10MB file size limit
  • Real-time progress tracking
  • Secure storage in Backblaze B2
  • AI usage tracking for storage operations
  • Error handling and validation
  • Mobile-responsive upload UI

Post-MVP Enhancements

  • Multiple file upload support
  • Google Docs integration
  • Dropbox/OneDrive import
  • Automatic metadata extraction
  • Virus scanning
  • Larger file size support
  • Resume interrupted uploads

Implementation Time

  • Development: 1 day
  • Testing: 0.5 days
  • Total: 1.5 days

Dependencies

  • F004-AI-SERVICES must be completed (for future processing)
  • Backblaze B2 bucket must be configured
  • @mystoryflow/shared storage utilities must be available

Next Feature

After completion, proceed to F006-CONTENT-EXTRACTION to extract text from uploaded files.