CASE STUDY · 01
English Journal: a pronunciation journal that listens to how you speak
You can read English just fine; now you need people to understand you. English Journal listens to how you speak, spots the sound you keep missing and decides what you practice tomorrow. No mascots, no lives, no empty rewards. And it works even offline.
- ROLE
- End-to-end design and development
- TYPE
- Pronunciation journal · Installable web app (PWA)
- STACK
- Next.js 16 · React 19 · Supabase · Gemini API · Dexie · Kokoro TTS
- STATUS
- In production · since 2025

THE PROBLEM
Language apps usually need a constant connection, reward streaks instead of real learning and, when they use AI, every answer costs money.
THE GOAL
An app you can open on the subway with no signal, that tells you honestly what you know and what you don’t, and only uses AI when it truly helps.
PROCESS
From a pronunciation notebook to a full app
- 01
Device first
Everything you do is saved instantly on your phone or computer. Cloud sync happens in the background, with automatic retries.
- 02
AI on a budget
Before calling Gemini, the app checks answers with its own rules. That saves calls and keeps corrections predictable.
- 03
Private data per user
139 Supabase migrations with row-level security on every table: each person can only see their own data.
- 04
Thoroughly tested
Around 6,600 automated tests with Vitest and Playwright cover everything from review logic to accessibility.
TECHNICAL DECISIONS
What happens behind the interface
Technical decisions to keep the app fast, never lose data and keep AI spending under control.
// lib/sync/recovery.ts: retries with growing delays
const RETRY_DELAYS_MS = [5_000, 30_000, 2 * 60_000]
export function getNextRetryAt(retryCount: number, attemptedAt: string): string {
const delay = RETRY_DELAYS_MS[Math.min(retryCount - 1, RETRY_DELAYS_MS.length - 1)]
return new Date(new Date(attemptedAt).getTime() + delay).toISOString()
}
/** Releases entries left in `syncing` by a crashed or closed tab. */
export async function reclaimStaleSyncingEntries(currentTime = Date.now()) {
const staleBefore = new Date(currentTime - SYNCING_STALE_MS).toISOString()
return db.syncOutbox
.where('status')
.equals('syncing')
.filter((entry) => Boolean(entry.lastAttemptAt && entry.lastAttemptAt < staleBefore))
.modify({ status: 'pending' })
}Local-first sync (Dexie + Supabase)
Answers are saved to IndexedDB first and go through an outbox queue. If a tab closes halfway, the app recovers those writes on its own.
Rules before the model
Deterministic validators and pre-generated content handle most exercises without calling Gemini live.
Voice that works offline
Text-to-speech runs with Kokoro right in the browser, with no server involved.
Spaced repetition with FSRS (ts-fsrs)
Mastery is calculated only from evaluated practice. Opening the app or keeping a streak doesn’t count as learning.
THE SOLUTION
Four pillars, one profile of your voice
Sound Lab
Record yourself, see your waveform next to a native speaker’s and find the sound you keep missing.
Essential words
The 2,800 words that show up in almost everything you will hear, with their real weak forms.
Your journal
Write about your day and get corrections explained in Spanish. Your vocabulary comes from your life.
Speaking missions
Conversations with a goal: order a coffee, explain a problem, defend an idea.
RESULTS
By the numbers
Automated tests
Unit, integration and end-to-end with Vitest and Playwright.
Essential words
Each with its real weak form and spaced review.
Mini lessons
From A1 to C2, plus 276 grammar pattern decks.
WHAT I LEARNED
An app that respects your time works on your device first and only uses the cloud when it needs to.
Handling with your own rules whatever doesn’t need a language model makes AI cheaper and its answers more predictable.
Reading is exposure, saving is intent and practice is evidence. Mastery is earned over time, not through clicks or streaks.