Lexora
A vocabulary app you use with your eyes closed — playlists that play from the lock screen, offline.
For a founder — cost, outcome, time
- Languages
- 12
- Built in
- 17 days
- New playlist
- ~1.5 s
- Status
- App Store
Learners already have vocabulary in Quizlet, Anki or a sheet — but every app needs eyes on the screen. Commutes, runs and chores don’t allow that.
Word-pair playlists that play from the lock screen, offline, in 12 languages.
- Flutter
- Supabase
- Google TTS Neural2
- Gemini 3.6 Flash
Lexora is a vocabulary app that never asks you to look at your phone — playlists of word pairs play through Neural2 voices while you drive, run, or cook.
Audio first. Twelve languages. Lock screen playable, offline capable, no eyes on glass. Everything else is in service of that.
Context
Language learners already have vocabulary — in Quizlet, Anki, Google Sheets. What they don't have is a way to use it hands-free. Duolingo demands eyes on a screen. Anki demands taps. Lexora just plays.
Problem
Every vocabulary app treats audio as an extra. In real use — commutes, workouts, chores — screens don't work. You need something that runs from a lock screen, plays in background, works offline (subways, flights), and doesn't force a language-pair schema (I want Ukrainian definitions of English business terms, not a preset "en→uk" course).
Division of Labor
| Layer | Who |
|---|---|
| Product decisions (12 languages, drop Farsi when Google has no fa-IR voice) | Me |
Feature spec (.md file first, always) | Claude via /speckit-specify |
| Task breakdown & priorities | Claude via /speckit-plan + /speckit-tasks |
| Implementation | Claude via /speckit-implement |
| Device QA, Transporter, entitlements, Supabase seed SQL | Me |
| DEVLOG entry after each session | Claude |
Prompt Architecture — Spec-First Discipline
Every feature — even sub-hour changes — writes a markdown spec before code. The /speckit-* family (specify → plan → tasks → implement → checklist) is a rail Claude runs on. There are 17 custom skills in .claude/skills/ — the speckit family plus a shiploop family for QA, brainstorm, and observer roles.
CLAUDE.md — the routing ruleprompt
If the change touches more than 3 files or takes more than an hour, it goes through speckit. If it's a bug, Claude diagnoses → fixes → verifies → DEVLOGs. No exceptions to the DEVLOG step — that's what makes the next session cheap.
Iteration Moment — Claude API → Gemini
AI Generate feature calls Anthropic's API. .env had CLAUDE_API_KEY= empty. Users hit 401. Feature was silently broken until QA.
Migrated generation_service.dart to Gemini 3.6 Flash. GCP account already had TTS + Translation, so this collapsed three API keys into two providers. responseMimeType: application/json guaranteed clean JSON. 20-word playlists generate in ~1.5s. Free tier.
Lesson: When the stack already has a cloud provider, use their model. Two API keys is a bug factory; one is a feature.
Trade-Offs
Silent MP3 assets over SilenceAudioSource
Just_audio's silence source crashed with error 9999999 on real devices. Solution: pre-generated MP3s for 1–5s pauses in assets/. Cost: +2 MB bundle. Value: playback that actually works.
Gemini over Claude / GPT-4 for generation
Free, one API key, JSON-guaranteed. Less nuanced on terminology — acceptable trade at MVP.
Postgres trigger for word_count
Trigger on words INSERT/DELETE updates playlists.word_count atomically. No app-level reconciliation. Cost: tiny latency. Value: zero stale counts, ever.
Offline-first cache with dual-write
Generate audio → save local file → queue Supabase upload. Playback latency under 100ms. Cost: if the app crashes pre-upload, Supabase is stale — re-sync on next open.
Results (honest)
Topic string → iOS lock-screen playback · the Build 15 steady state
- Learner · person / role
- Signal / data · data / signal
- Data store
- Server work · process step
- Gate · decision / gate
- Outcome
- Flow
- Key path / outcome
Read as steps (9)
- 01Learner → Topic string — Generate with AI
- 02Topic string → AI consent
- 03AI consent → Supabase Edge Fn — granted
- 04Supabase Edge Fn → Gemini 3.6 Flash — signed call
- 05Gemini 3.6 Flash → Playlist words — 8 pairs · JSON
- 06Playlist words → Google TTS Neural2 — per-word render
- 07Google TTS Neural2 → MP3 cache — MP3 per track
- 08MP3 cache → just_audio — offline-first
- 09just_audio → iOS lock screen (✓ approved)
Build 12 → Build 15 · the Apple review arc
BeforeBuild 12 → Build 14 — rejected on undisclosed third-party AI
2026-09-14 → 09-24
- Process step
- Decision / gate
- Outcome
- Flow
Read as steps (4)
- 01Build 12 → Build 13
- 02Build 13 → Build 14
- 03Build 14 → Apple review
- 04Apple review → Rejected (✕ denied)
AfterBuild 15 — explicit consent before any topic leaves the device
2026-09-29 → 2026-10-08 · approved
- Data / signal
- Process step
- Outcome
- Flow
- Key path / outcome
Read as steps (4)
- 01Rejection as spec → Consent gate
- 02Consent gate → AI disclosure UI
- 03AI disclosure UI → Resubmit Build 15
- 04Resubmit Build 15 → Approved (✓ approved)
Reflection
A rotating API key is a runtime dependency, not a config. Two Build resubmissions came from key rotations that shell env didn't pick up.
- Spec-first isn't ceremony — it's cache. The
specs/folder is a durable record. Next session, Claude re-reads the spec instead of me re-explaining. Front-load once, save every session after. - Real devices ≠ simulators. Both crashes that made it into DEVLOG (
SilenceAudioSource, stale env keys) only reproduced on hardware. Simulator green means nothing. - Build-pass discipline closes the loop.
--dart-define-from-file=.envis now the only build path. One rejection becomes one rule, enforced in code — not in a README.
Receipts
How it was made
Token scorecard
Pre-ShipLoop sessions ran in shared home-dir logs; token attribution isn’t reconstructable.
- Complexity
- M
- Duration
- 17 days
- Tier
- 0 · naked
Model mix
- Opus
- 15%
- Sonnet
- 85%
Build timeline
31 days from Aug 30 to Sep 29, 3 commits
- Phases
- 2
- Days
- 31
- Peak day
- —
- Peak commits
- 2
Have a Lexora-sized problem?
Send one paragraph about it. I reply within two business days with a scope and a fixed price.