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
- 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
- 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
- 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
| Feature | Parent Mode | Kid Mode | Family Mode |
|---|---|---|---|
| Lessons | Business phrases, meeting templates | ABCs, basic vocabulary, phonics | Story time, conversation practice |
| Practice | AI role-play, speech recording | Games, puzzles, matching | Collaborative quizzes |
| AI Assistant | Claude API for feedback | Age-appropriate hints | Family conversation prompts |
| Gamification | Streak tracker, level badges | Stars, characters, animations | Family achievement board |
| Speech | Pronunciation scoring | Listen-and-repeat | Read-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
| Aspect | React Native + Expo | Next.js Web App |
|---|---|---|
| Platform | iOS + Android native | Browser (all devices) |
| Offline | Full offline with SQLite | Service Worker + IndexedDB |
| Speech | expo-speech + expo-av | Web Speech API |
| Push notifications | Native (expo-notifications) | Web Push API |
| Camera/AR | Full native access | Limited browser access |
| Distribution | App Store / Play Store | Any URL (instant access) |
| Development speed | Medium (need device testing) | Fast (browser only) |
| Best for | Long-term product | MVP / 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
| Service | Monthly 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?
- Customized for your exact needs — no business English app teaches “explain a microservices migration to a VP”
- Your children’s exact level — customize content for their age and learning pace
- Family connection — no existing app has a parent+child shared mode
- Your portfolio — this is a real product you can showcase as a tech lead
- AI-native — built with AI from the ground up, not AI bolted on as a feature
- 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.