Throughout this series, I’ve talked about the tools and techniques for learning English. But there’s one tool I haven’t mentioned — the one you build yourself.

As a tech lead who codes with AI, you can build any app you want. So why not build one that solves your exact problem? An app that helps you practice business English AND helps your children learn English fundamentals — at the same time, together.

This post is a complete technical blueprint. Not a tutorial with toy examples — a production-grade specification you can actually implement. I’ll cover both React Native + Expo (cross-platform mobile) and Next.js (web app) approaches so you can choose the one that fits your skills.

Product Vision

The Problem

  1. For you (the parent): You need business English practice but traditional apps teach conversational/tourist English, not “explain a race condition to a VP” English
  2. For your children: They need fun, game-based English learning, but you want it to connect to your learning journey so you can practice together
  3. For the family: No existing app provides a shared learning experience where parent and child both grow

The Solution: FamilyLingo

A dual-mode English learning app:

  • Parent Mode: AI-powered business English with role-play scenarios, vocabulary for tech meetings, and pronunciation drills
  • Kid Mode: Gamified learning with colorful characters, rewards, and age-appropriate content
  • Family Mode: Shared challenges where parent and child practice together

Core Features

FeatureParent ModeKid ModeFamily Mode
LessonsBusiness phrases, meeting templatesABCs, basic vocabulary, phonicsStory time, conversation practice
PracticeAI role-play, speech recordingGames, puzzles, matchingCollaborative quizzes
AI AssistantClaude API for feedbackAge-appropriate hintsFamily conversation prompts
GamificationStreak tracker, level badgesStars, characters, animationsFamily achievement board
SpeechPronunciation scoringListen-and-repeatRead-aloud together

Architecture Overview

High-Level Architecture

┌─────────────────────────────────────────────────┐
│                  Frontend                        │
│  ┌─────────────┐  ┌──────────────────────────┐  │
│  │ React Native│  │      Next.js Web App      │  │
│  │  + Expo     │  │   (Alternative Option)    │  │
│  └──────┬──────┘  └────────────┬─────────────┘  │
│         └──────────┬───────────┘                 │
│                    │                             │
│  ┌─────────────────▼──────────────────────────┐  │
│  │         Shared API Layer                    │  │
│  │  REST/GraphQL + WebSocket                   │  │
│  └─────────────────┬──────────────────────────┘  │
│                    │                             │
│  ┌─────────────────▼──────────────────────────┐  │
│  │           Backend Services                  │  │
│  │  ┌────────┐  ┌───────┐  ┌──────────────┐   │  │
│  │  │  Auth  │  │Lessons│  │ AI Engine    │   │  │
│  │  │Service │  │Service│  │ (Claude API) │   │  │
│  │  └────────┘  └───────┘  └──────────────┘   │  │
│  │  ┌────────┐  ┌───────┐  ┌──────────────┐   │  │
│  │  │Progress│  │Speech │  │ Gamification │   │  │
│  │  │Tracker │  │Engine │  │   Engine     │   │  │
│  │  └────────┘  └───────┘  └──────────────┘   │  │
│  └─────────────────┬──────────────────────────┘  │
│                    │                             │
│  ┌─────────────────▼──────────────────────────┐  │
│  │           Data Layer                        │  │
│  │  PostgreSQL + Redis Cache + S3 Storage      │  │
│  └─────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

Tech Stack Comparison

AspectReact Native + ExpoNext.js Web App
PlatformiOS + Android nativeBrowser (all devices)
OfflineFull offline with SQLiteService Worker + IndexedDB
Speechexpo-speech + expo-avWeb Speech API
Push notificationsNative (expo-notifications)Web Push API
Camera/ARFull native accessLimited browser access
DistributionApp Store / Play StoreAny URL (instant access)
Development speedMedium (need device testing)Fast (browser only)
Best forLong-term productMVP / quick validation

My recommendation: Start with Next.js for your MVP. It’s faster to build, easier to test, and you can share the URL with your family immediately. If it works well, migrate to React Native later for the native experience.

Option A: React Native + Expo Implementation

Project Setup

# Create new Expo project
npx create-expo-app@latest FamilyLingo --template blank-typescript

# Install core dependencies
cd FamilyLingo
npx expo install expo-router expo-speech expo-av expo-haptics
npx expo install @react-native-async-storage/async-storage
npm install @anthropic-ai/sdk zustand react-native-reanimated
npm install @supabase/supabase-js

Folder Structure

FamilyLingo/
├── app/
│   ├── (tabs)/
│   │   ├── _layout.tsx         # Tab navigation
│   │   ├── index.tsx           # Home/Dashboard
│   │   ├── learn.tsx           # Lesson selection
│   │   ├── practice.tsx        # Practice mode
│   │   ├── family.tsx          # Family challenges
│   │   └── profile.tsx         # Profile & settings
│   ├── lesson/
│   │   ├── [id].tsx            # Individual lesson view
│   │   └── quiz.tsx            # Quiz for lesson
│   ├── roleplay/
│   │   └── [scenario].tsx      # AI role-play screen
│   ├── auth/
│   │   ├── login.tsx
│   │   └── register.tsx
│   └── _layout.tsx             # Root layout
├── components/
│   ├── ui/                     # Reusable UI components
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   ├── ProgressBar.tsx
│   │   └── Badge.tsx
│   ├── parent/                 # Parent-mode components
│   │   ├── PhraseCard.tsx
│   │   ├── RolePlayChat.tsx
│   │   └── PronunciationDrill.tsx
│   ├── kid/                    # Kid-mode components
│   │   ├── AnimalCard.tsx
│   │   ├── MatchingGame.tsx
│   │   ├── PhonicsWheel.tsx
│   │   └── StarReward.tsx
│   └── family/                 # Family-mode components
│       ├── FamilyBoard.tsx
│       ├── SharedChallenge.tsx
│       └── StoryReader.tsx
├── services/
│   ├── ai.ts                   # Claude/OpenAI API integration
│   ├── speech.ts               # Speech recognition & synthesis
│   ├── auth.ts                 # Authentication
│   └── supabase.ts             # Supabase client
├── store/
│   ├── useAuth.ts              # Auth state
│   ├── useProgress.ts          # Learning progress
│   ├── useLessons.ts           # Lesson data
│   └── useFamily.ts            # Family state
├── data/
│   ├── parent-lessons.json     # Business English content
│   ├── kid-lessons.json        # Kids content
│   └── family-challenges.json  # Family activities
├── utils/
│   ├── scoring.ts              # Pronunciation scoring
│   └── gamification.ts         # XP, levels, badges
└── constants/
    └── theme.ts                # Colors, fonts, spacing

Key Component: AI Role-Play Chat

// components/parent/RolePlayChat.tsx
import React, { useState, useRef } from 'react';
import { View, Text, ScrollView, TextInput, Pressable } from 'react-native';
import Anthropic from '@anthropic-ai/sdk';

interface Message {
  role: 'user' | 'assistant' | 'system';
  content: string;
}

interface RolePlayProps {
  scenario: {
    title: string;
    systemPrompt: string;
    openingLine: string;
    feedbackPrompt: string;
  };
}

export function RolePlayChat({ scenario }: RolePlayProps) {
  const [messages, setMessages] = useState<Message[]>([
    { role: 'assistant', content: scenario.openingLine }
  ]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [feedback, setFeedback] = useState<string | null>(null);
  const scrollRef = useRef<ScrollView>(null);

  const sendMessage = async () => {
    if (!input.trim() || isLoading) return;

    const userMessage: Message = { role: 'user', content: input };
    const updatedMessages = [...messages, userMessage];
    setMessages(updatedMessages);
    setInput('');
    setIsLoading(true);

    try {
      // Call your backend API (don't expose API keys in mobile app)
      const response = await fetch('/api/roleplay', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          systemPrompt: scenario.systemPrompt,
          messages: updatedMessages,
        }),
      });
      
      const data = await response.json();
      setMessages(prev => [...prev, { 
        role: 'assistant', 
        content: data.reply 
      }]);
    } catch (error) {
      console.error('AI response error:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const requestFeedback = async () => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/roleplay/feedback', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messages,
          feedbackPrompt: scenario.feedbackPrompt,
        }),
      });
      const data = await response.json();
      setFeedback(data.feedback);
    } catch (error) {
      console.error('Feedback error:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <View style={{ flex: 1 }}>
      <ScrollView ref={scrollRef} style={{ flex: 1, padding: 16 }}>
        {messages.map((msg, i) => (
          <View
            key={i}
            style={{
              alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
              backgroundColor: msg.role === 'user' ? '#4527A0' : '#F5F5F5',
              borderRadius: 16,
              padding: 12,
              marginBottom: 8,
              maxWidth: '80%',
            }}
          >
            <Text style={{ 
              color: msg.role === 'user' ? '#FFF' : '#333',
              fontSize: 16 
            }}>
              {msg.content}
            </Text>
          </View>
        ))}
      </ScrollView>

      {feedback && (
        <View style={{ 
          backgroundColor: '#E8F5E9', 
          padding: 16, 
          margin: 8, 
          borderRadius: 12 
        }}>
          <Text style={{ fontWeight: 'bold', marginBottom: 8 }}>
            AI Feedback
          </Text>
          <Text>{feedback}</Text>
        </View>
      )}

      <View style={{ 
        flexDirection: 'row', 
        padding: 8, 
        borderTopWidth: 1, 
        borderColor: '#E0E0E0' 
      }}>
        <TextInput
          value={input}
          onChangeText={setInput}
          placeholder="Type your response..."
          style={{ 
            flex: 1, 
            borderWidth: 1, 
            borderColor: '#CCC', 
            borderRadius: 24, 
            paddingHorizontal: 16, 
            paddingVertical: 8, 
            marginRight: 8 
          }}
          onSubmitEditing={sendMessage}
        />
        <Pressable
          onPress={sendMessage}
          style={{ 
            backgroundColor: '#4527A0', 
            borderRadius: 24, 
            width: 48, 
            height: 48, 
            justifyContent: 'center', 
            alignItems: 'center' 
          }}
        >
          <Text style={{ color: '#FFF', fontSize: 20 }}></Text>
        </Pressable>
      </View>

      <Pressable
        onPress={requestFeedback}
        style={{ 
          backgroundColor: '#2E7D32', 
          margin: 8, 
          padding: 12, 
          borderRadius: 8, 
          alignItems: 'center' 
        }}
      >
        <Text style={{ color: '#FFF', fontWeight: 'bold' }}>
          Get AI Feedback on My English
        </Text>
      </Pressable>
    </View>
  );
}

Key Component: Kid Mode Matching Game

// components/kid/MatchingGame.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, Pressable, Image } from 'react-native';
import * as Haptics from 'expo-haptics';
import * as Speech from 'expo-speech';
import Animated, { 
  useSharedValue, 
  useAnimatedStyle, 
  withSpring,
  withSequence 
} from 'react-native-reanimated';

interface Card {
  id: string;
  type: 'word' | 'image';
  value: string;
  matchId: string;
  image?: string;
}

interface MatchingGameProps {
  cards: Card[];
  onComplete: (score: number) => void;
}

export function MatchingGame({ cards, onComplete }: MatchingGameProps) {
  const [flipped, setFlipped] = useState<Set<string>>(new Set());
  const [matched, setMatched] = useState<Set<string>>(new Set());
  const [selected, setSelected] = useState<Card | null>(null);
  const [score, setScore] = useState(0);

  const handleCardPress = (card: Card) => {
    if (flipped.has(card.id) || matched.has(card.id)) return;

    // Speak the word when tapped
    if (card.type === 'word') {
      Speech.speak(card.value, { language: 'en-US', rate: 0.8 });
    }

    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    setFlipped(prev => new Set(prev).add(card.id));

    if (!selected) {
      setSelected(card);
    } else {
      // Check match
      if (selected.matchId === card.matchId) {
        // Match found!
        Haptics.notificationAsync(
          Haptics.NotificationFeedbackType.Success
        );
        setMatched(prev => {
          const next = new Set(prev);
          next.add(selected.id);
          next.add(card.id);
          return next;
        });
        setScore(prev => prev + 10);
        
        if (matched.size + 2 === cards.length) {
          onComplete(score + 10);
        }
      } else {
        // No match — flip back after delay
        Haptics.notificationAsync(
          Haptics.NotificationFeedbackType.Error
        );
        setTimeout(() => {
          setFlipped(prev => {
            const next = new Set(prev);
            next.delete(selected.id);
            next.delete(card.id);
            return next;
          });
        }, 1000);
      }
      setSelected(null);
    }
  };

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <View style={{ 
        flexDirection: 'row', 
        justifyContent: 'space-between', 
        marginBottom: 16 
      }}>
        <Text style={{ fontSize: 24, fontWeight: 'bold' }}>
          ⭐ {score}
        </Text>
        <Text style={{ fontSize: 18, color: '#666' }}>
          {matched.size / 2} / {cards.length / 2} matched
        </Text>
      </View>

      <View style={{ 
        flexDirection: 'row', 
        flexWrap: 'wrap', 
        justifyContent: 'center', 
        gap: 8 
      }}>
        {cards.map(card => (
          <Pressable
            key={card.id}
            onPress={() => handleCardPress(card)}
            style={{
              width: 80,
              height: 100,
              borderRadius: 12,
              backgroundColor: matched.has(card.id) 
                ? '#C8E6C9' 
                : flipped.has(card.id) 
                  ? '#E3F2FD' 
                  : '#4527A0',
              justifyContent: 'center',
              alignItems: 'center',
              elevation: 4,
            }}
          >
            {flipped.has(card.id) || matched.has(card.id) ? (
              card.type === 'image' ? (
                <Text style={{ fontSize: 32 }}>{card.image}</Text>
              ) : (
                <Text style={{ 
                  fontSize: 14, 
                  fontWeight: 'bold', 
                  textAlign: 'center' 
                }}>
                  {card.value}
                </Text>
              )
            ) : (
              <Text style={{ fontSize: 32, color: '#FFF' }}>?</Text>
            )}
          </Pressable>
        ))}
      </View>
    </View>
  );
}

Option B: Next.js Web App Implementation

Project Setup

# Create Next.js project
npx create-next-app@latest family-lingo --typescript --tailwind \
  --eslint --app --src-dir --import-alias "@/*"

cd family-lingo

# Install dependencies
npm install @anthropic-ai/sdk @supabase/supabase-js
npm install framer-motion lucide-react
npm install @prisma/client prisma

Folder Structure

family-lingo/
├── src/
│   ├── app/
│   │   ├── layout.tsx              # Root layout
│   │   ├── page.tsx                # Landing page
│   │   ├── (auth)/
│   │   │   ├── login/page.tsx
│   │   │   └── register/page.tsx
│   │   ├── (parent)/
│   │   │   ├── dashboard/page.tsx  # Parent dashboard
│   │   │   ├── lessons/page.tsx    # Business English lessons
│   │   │   ├── roleplay/
│   │   │   │   ├── page.tsx        # Scenario selection
│   │   │   │   └── [id]/page.tsx   # Active role-play
│   │   │   ├── vocabulary/page.tsx # Phrase bank
│   │   │   └── pronunciation/page.tsx
│   │   ├── (kid)/
│   │   │   ├── play/page.tsx       # Kid dashboard
│   │   │   ├── games/
│   │   │   │   ├── matching/page.tsx
│   │   │   │   ├── phonics/page.tsx
│   │   │   │   └── stories/page.tsx
│   │   │   └── rewards/page.tsx
│   │   ├── (family)/
│   │   │   ├── challenges/page.tsx
│   │   │   └── board/page.tsx      # Family leaderboard
│   │   └── api/
│   │       ├── ai/
│   │       │   ├── roleplay/route.ts
│   │       │   ├── feedback/route.ts
│   │       │   └── pronunciation/route.ts
│   │       ├── lessons/route.ts
│   │       └── progress/route.ts
│   ├── components/
│   │   ├── ui/                     # Design system
│   │   ├── parent/                 # Parent mode
│   │   ├── kid/                    # Kid mode  
│   │   └── family/                 # Family mode
│   ├── lib/
│   │   ├── ai.ts                   # AI client
│   │   ├── supabase.ts             # DB client
│   │   └── speech.ts               # Web Speech API
│   ├── hooks/
│   │   ├── useProgress.ts
│   │   ├── useSpeech.ts
│   │   └── useAI.ts
│   └── data/
│       ├── parent-lessons.ts
│       ├── kid-content.ts
│       └── scenarios.ts
├── prisma/
│   └── schema.prisma
└── public/
    └── sounds/                     # Audio files

Database Schema (Prisma)

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String
  role          UserRole  @default(PARENT)
  avatar        String?
  createdAt     DateTime  @default(now())
  
  family        Family?   @relation(fields: [familyId], references: [id])
  familyId      String?
  progress      Progress[]
  phrases       Phrase[]
  streaks       Streak[]
  achievements  Achievement[]
}

model Family {
  id          String   @id @default(cuid())
  name        String
  createdAt   DateTime @default(now())
  
  members     User[]
  challenges  FamilyChallenge[]
}

model Lesson {
  id          String     @id @default(cuid())
  title       String
  description String
  mode        LessonMode
  level       Int        @default(1)
  category    String
  content     Json       // Flexible content structure
  order       Int
  
  progress    Progress[]
}

model Progress {
  id          String   @id @default(cuid())
  userId      String
  lessonId    String
  score       Int      @default(0)
  completed   Boolean  @default(false)
  timeSpent   Int      @default(0) // seconds
  attempts    Int      @default(0)
  lastAttempt DateTime @default(now())
  
  user        User     @relation(fields: [userId], references: [id])
  lesson      Lesson   @relation(fields: [lessonId], references: [id])

  @@unique([userId, lessonId])
}

model Phrase {
  id          String   @id @default(cuid())
  userId      String
  phrase      String
  category    String
  context     String
  example     String
  learned     DateTime @default(now())
  lastUsed    DateTime?
  timesUsed   Int      @default(0)
  
  user        User     @relation(fields: [userId], references: [id])
}

model Streak {
  id          String   @id @default(cuid())
  userId      String
  date        DateTime @db.Date
  minutes     Int      @default(0)
  
  user        User     @relation(fields: [userId], references: [id])

  @@unique([userId, date])
}

model Achievement {
  id          String   @id @default(cuid())
  userId      String
  type        String
  name        String
  earnedAt    DateTime @default(now())
  
  user        User     @relation(fields: [userId], references: [id])
}

model FamilyChallenge {
  id          String   @id @default(cuid())
  familyId    String
  title       String
  description String
  type        String
  targetScore Int
  currentScore Int     @default(0)
  startDate   DateTime
  endDate     DateTime
  completed   Boolean  @default(false)
  
  family      Family   @relation(fields: [familyId], references: [id])
}

enum UserRole {
  PARENT
  CHILD
}

enum LessonMode {
  PARENT
  KID
  FAMILY
}

AI API Route

// src/app/api/ai/roleplay/route.ts
import Anthropic from '@anthropic-ai/sdk';
import { NextRequest, NextResponse } from 'next/server';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
});

export async function POST(req: NextRequest) {
  const { systemPrompt, messages, mode } = await req.json();

  const systemMessage = mode === 'kid' 
    ? `You are a friendly English teacher for children aged 4-10. 
       Use simple words, short sentences, and lots of encouragement. 
       Include emojis. Never use complex grammar terminology.
       ${systemPrompt}`
    : `You are a professional English communication coach for 
       non-native speaking tech leads. You understand software 
       engineering concepts. Give practical, specific feedback.
       Rate clarity on a 1-10 scale when asked.
       ${systemPrompt}`;

  try {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: systemMessage,
      messages: messages.map((m: any) => ({
        role: m.role,
        content: m.content,
      })),
    });

    const reply = response.content[0].type === 'text' 
      ? response.content[0].text 
      : '';

    return NextResponse.json({ reply });
  } catch (error) {
    console.error('Anthropic API error:', error);
    return NextResponse.json(
      { error: 'AI service unavailable' },
      { status: 500 }
    );
  }
}

Web Speech API Integration

// src/lib/speech.ts

export class SpeechService {
  private recognition: SpeechRecognition | null = null;
  private synthesis: SpeechSynthesisUtterance | null = null;

  constructor() {
    if (typeof window !== 'undefined') {
      const SpeechRecognition = 
        window.SpeechRecognition || 
        (window as any).webkitSpeechRecognition;
      
      if (SpeechRecognition) {
        this.recognition = new SpeechRecognition();
        this.recognition.lang = 'en-US';
        this.recognition.continuous = false;
        this.recognition.interimResults = true;
      }
    }
  }

  // Text to Speech — read text aloud
  speak(text: string, options?: {
    rate?: number;    // 0.1 to 10, default 1
    pitch?: number;   // 0 to 2, default 1
    voice?: string;   // voice name
  }): Promise<void> {
    return new Promise((resolve, reject) => {
      const utterance = new SpeechSynthesisUtterance(text);
      utterance.lang = 'en-US';
      utterance.rate = options?.rate ?? 0.9; // Slightly slower for learners
      utterance.pitch = options?.pitch ?? 1;
      
      if (options?.voice) {
        const voices = speechSynthesis.getVoices();
        const voice = voices.find(v => v.name === options.voice);
        if (voice) utterance.voice = voice;
      }

      utterance.onend = () => resolve();
      utterance.onerror = (e) => reject(e);
      speechSynthesis.speak(utterance);
    });
  }

  // Speech to Text — listen to user
  listen(): Promise<string> {
    return new Promise((resolve, reject) => {
      if (!this.recognition) {
        reject('Speech recognition not supported');
        return;
      }

      this.recognition.onresult = (event) => {
        const transcript = Array.from(event.results)
          .map(result => result[0].transcript)
          .join('');
        resolve(transcript);
      };

      this.recognition.onerror = (event) => reject(event.error);
      this.recognition.start();
    });
  }

  stop() {
    this.recognition?.stop();
    speechSynthesis.cancel();
  }
}

Content Design

Parent Lesson Categories

// src/data/parent-lessons.ts
export const parentLessons = {
  categories: [
    {
      id: 'meetings',
      title: 'Meeting English',
      icon: '🏢',
      lessons: [
        {
          id: 'meeting-openers',
          title: 'Opening a Meeting',
          level: 1,
          phrases: [
            {
              phrase: "Let's get started",
              context: 'Beginning a meeting',
              pronunciation: '/lets ɡɛt ˈstɑːrtɪd/',
              example: "Thanks for joining, everyone. Let's get started.",
              alternatives: [
                "Shall we begin?",
                "Let's dive in.",
                "Let's kick things off."
              ]
            },
            // ... more phrases
          ],
          roleplay: {
            title: 'Sprint Planning Meeting',
            systemPrompt: 'You are a product owner...',
            openingLine: "Good morning, team. What do we have on the agenda today?"
          }
        },
        // ... more lessons
      ]
    },
    {
      id: 'explaining',
      title: 'Explaining Tech',
      icon: '🧩',
      lessons: [/* ... */]
    },
    {
      id: 'presenting',
      title: 'Presentations',
      icon: '🎤',
      lessons: [/* ... */]
    },
    {
      id: 'email',
      title: 'Email & Slack',
      icon: '📧',
      lessons: [/* ... */]
    },
    {
      id: 'negotiation',
      title: 'Negotiation',
      icon: '🤝',
      lessons: [/* ... */]
    }
  ]
};

Kid Lesson Categories

// src/data/kid-content.ts
export const kidLessons = {
  ages: {
    '3-5': [
      {
        category: 'alphabet',
        title: 'ABC Adventures',
        icon: '🔤',
        games: [
          {
            type: 'matching',
            title: 'Match the Letter',
            cards: [
              { word: 'Apple', image: '🍎', letter: 'A' },
              { word: 'Ball', image: '⚽', letter: 'B' },
              { word: 'Cat', image: '🐱', letter: 'C' },
              // ...
            ]
          },
          {
            type: 'phonics',
            title: 'Sound Safari',
            sounds: [
              { letter: 'A', sound: '/æ/', word: 'ant', image: '🐜' },
              { letter: 'B', sound: '/b/', word: 'bear', image: '🐻' },
              // ...
            ]
          }
        ]
      },
      {
        category: 'numbers',
        title: 'Number Land',
        icon: '🔢',
        games: [/* count objects, number matching */]
      },
      {
        category: 'colors',
        title: 'Color World',
        icon: '🌈',
        games: [/* color identification, mixing */]
      }
    ],
    '6-10': [
      {
        category: 'vocabulary',
        title: 'Word Explorer',
        icon: '📚',
        games: [/* vocabulary building, sentence completion */]
      },
      {
        category: 'grammar',
        title: 'Grammar Galaxy',
        icon: '🚀',
        games: [/* basic grammar, sentence ordering */]
      },
      {
        category: 'reading',
        title: 'Story Time',
        icon: '📖',
        games: [/* reading comprehension, story creation */]
      }
    ]
  }
};

Gamification System

XP and Leveling

// src/utils/gamification.ts
export const GAMIFICATION = {
  xp: {
    lessonComplete: 50,
    quizPerfect: 100,
    roleplaySession: 75,
    dailyStreak: 25,
    familyChallenge: 150,
    pronunciationDrill: 30,
    newPhrase: 10,
  },
  
  levels: [
    { level: 1, title: 'Beginner',      xpRequired: 0 },
    { level: 2, title: 'Explorer',       xpRequired: 200 },
    { level: 3, title: 'Communicator',   xpRequired: 500 },
    { level: 4, title: 'Confident',      xpRequired: 1000 },
    { level: 5, title: 'Professional',   xpRequired: 2000 },
    { level: 6, title: 'Fluent',         xpRequired: 4000 },
    { level: 7, title: 'Master',         xpRequired: 8000 },
    { level: 8, title: 'Native-Level',   xpRequired: 15000 },
  ],

  kidTitles: [
    { level: 1, title: 'Little Star',        emoji: '⭐' },
    { level: 2, title: 'Word Wizard',        emoji: '🧙' },
    { level: 3, title: 'Super Reader',       emoji: '📖' },
    { level: 4, title: 'Language Hero',      emoji: '🦸' },
    { level: 5, title: 'English Champion',   emoji: '🏆' },
  ],

  achievements: [
    { id: 'first-lesson',    name: 'First Step',       condition: 'Complete 1 lesson' },
    { id: 'week-streak',     name: '7-Day Streak',     condition: '7 consecutive days' },
    { id: 'month-streak',    name: '30-Day Warrior',   condition: '30 consecutive days' },
    { id: 'phrase-collector', name: 'Phrase Collector', condition: '50 phrases saved' },
    { id: 'role-player',     name: 'Role Player',      condition: '10 role-play sessions' },
    { id: 'family-team',     name: 'Family Team',      condition: 'Complete family challenge' },
    { id: 'presenter',       name: 'Presenter',        condition: 'Record 5 presentation practices' },
  ]
};

export function calculateLevel(xp: number) {
  const levels = GAMIFICATION.levels;
  for (let i = levels.length - 1; i >= 0; i--) {
    if (xp >= levels[i].xpRequired) {
      const nextLevel = levels[i + 1];
      return {
        level: levels[i].level,
        title: levels[i].title,
        currentXp: xp - levels[i].xpRequired,
        nextLevelXp: nextLevel 
          ? nextLevel.xpRequired - levels[i].xpRequired 
          : null,
        progress: nextLevel 
          ? (xp - levels[i].xpRequired) / 
            (nextLevel.xpRequired - levels[i].xpRequired) 
          : 1,
      };
    }
  }
  return { 
    level: 1, 
    title: 'Beginner', 
    currentXp: 0, 
    nextLevelXp: 200, 
    progress: 0 
  };
}

Deployment Options

Option 1: Vercel (Next.js)

# Deploy in one command
npx vercel --prod

Cost: Free tier includes 100GB bandwidth, serverless functions. Good for family use.

Option 2: Cloudflare Pages (Next.js with @cloudflare/next-on-pages)

npm install @cloudflare/next-on-pages
npx wrangler pages deploy

Cost: Free tier is very generous. Good for global performance.

Option 3: Expo EAS (React Native)

npx eas build --platform all
npx eas submit --platform all

Cost: Free tier for builds. App Store ($99/year) and Play Store ($25 one-time) fees apply.

Backend: Supabase

  • Free tier: 500MB database, 1GB file storage, 50,000 monthly active users
  • Auth with social login built-in
  • Real-time subscriptions for family features
  • Perfect for family-scale apps

Implementation Roadmap

Week 1-2: Foundation

  • Project setup (Next.js or Expo)
  • Auth with Supabase
  • Database schema and migration
  • Basic navigation and routing
  • Design system (colors, typography, components)

Week 3-4: Parent Mode

  • Lesson data structure and content
  • Lesson view with phrase cards
  • AI role-play integration
  • Phrase bank (save/review)
  • Pronunciation drill with Web Speech API

Week 5-6: Kid Mode

  • Matching game component
  • Phonics wheel
  • Animated rewards (stars, badges)
  • Age-appropriate lesson content
  • Sound effects and haptics

Week 7-8: Family Mode + Polish

  • Family challenge system
  • Shared leaderboard
  • Streak tracking and notifications
  • Achievement system
  • Performance optimization and testing

Week 9-10: Launch

  • Production deployment
  • Analytics setup
  • Error monitoring
  • User feedback mechanism
  • Content expansion

Cost Estimate

ServiceMonthly Cost (Family Use)
Vercel / Cloudflare$0 (free tier)
Supabase$0 (free tier)
Claude API~$5-15 (depending on usage)
Domain~$1
Total~$6-16/month

For a family learning app, you can run this for essentially the cost of one Claude API subscription. Compare that to language learning apps that charge $15-20/month per person.

Why Build Instead of Buy?

  1. Customized for your exact needs — no business English app teaches “explain a microservices migration to a VP”
  2. Your children’s exact level — customize content for their age and learning pace
  3. Family connection — no existing app has a parent+child shared mode
  4. Your portfolio — this is a real product you can showcase as a tech lead
  5. AI-native — built with AI from the ground up, not AI bolted on as a feature
  6. Privacy — your family’s learning data stays with you

The best learning tool is the one you actually use. And you’ll use the one you built.


Next up: Part 8 — The 15-Minute Daily Routine — bringing everything together into one sustainable daily practice.

Export for reading

Comments