Six months of building Bookverse, alone
There’s a romantic version of building software alone where you have a clean desk, an espresso, and you ship something people love by lunchtime. The version I’ve actually lived for the past six months is more like “write the same Nginx config four times because the seventh edge case just bit you.”
This is a reflection — not a checklist of what’s shipped, not a tutorial. Just notes from six months in.
Why I started
I’d been using language apps on and off for years and noticed something specific: I could finish a 30-day streak on the popular gamified app and still not be able to read a paragraph of native text. The feedback loop was tight, the gamification was slick, but the unit of progress was a sentence, sometimes a word. And reading languages — actually reading them — happens at the level of pages.
I wanted an app that treated a textbook like a textbook. Open chapter one. Read it. Listen to it. Speak it back. Study what stuck. Tomorrow, chapter two.
The shape of the product was clear from day one. The catch is that “shape clear from day one” is responsible for maybe 5% of building software. The other 95% is the unglamorous middle.
The unglamorous middle
A non-exhaustive list of things I did this quarter that aren’t in any release notes:
- Renamed an entire monorepo path tree from
app_v54_01tobookverse(and re-renamed when I wanted underscores in the systemd unit name). - Wrote four versions of a Terraform config for Nginx vhosts before
discovering that a
null_resourcetrigger needed to include the rendered template’s hash, not just the input variables. - Spent a week tracking down why an
EXPO_PUBLIC_*env value resolved to an empty string in the web bundle but worked fine on iOS. (Babel’spreset-exposkips inlining fornode_modules. Factory functions, not env vars, in published packages.) - Refactored 17 callers of an API URL helper because I’d centralised it incorrectly the first time.
None of this would be in a marketing post about the app. All of it was the actual work.
The thing nobody warns you about solo development is that there’s no one to absorb the boring half. You don’t get to say “the platform team handles deploys” because you are the platform team. You wear every hat poorly until you’ve worn it long enough to wear it well.
The speech-recognition rabbit hole
The most instructive episode of the last six months was getting speaking practice working on phones in China.
The plan: tap a line, hear a native speaker say it, tap again, record
yourself, see how close you got. Standard mobile speech recognition —
expo-speech-recognition on iOS, the system service on Android, done.
Reality: a chunk of the target audience uses Lenovo Motorola phones
with the China-region OS variant (the gmsconfig.china overlay), which
strips out Google Mobile Services entirely. No system speech-to-text.
The app worked perfectly in the emulator. On a Moto XT2507 in Beijing,
it crashed silently.
The fix took about three weeks:
- Build an adapter pattern so the app can swap speech backends per platform — system recognition where available, cloud transcription as a fallback.
- Add a Whisper-style cloud backend (Alibaba’s
qwen3-asr-flashvia DashScope, since China-region devices can reach it without a VPN). - Tune voice activity detection so the recording auto-stops when you finish speaking. (-25 dBFS threshold, 600ms silence window, smoothed over five samples — every other combination either cut you off mid-word or never stopped at all.)
- Switch from React Native
FormDatauploads toexpo-file-system’suploadAsync, because RNFormDatawas flaky with audio blobs on the same devices.
Half of those four bullets is more code than I write in a typical week. None of it is “a feature.” All of it was necessary for the feature to exist for the people who needed it.
The lesson — repeated approximately every two weeks — is that the hard part is rarely the part you think will be hard. I budgeted a day for speech recognition. It took twenty.
The discipline of finishing
The temptation when building solo is to chase the new shiny. A new feature is exciting. The 47th tweak to an existing flow is not. So you accumulate features, none of which are quite finished, and the product feels like a graveyard of half-builds.
The discipline I keep relearning: finish one thing properly before starting the next. Mandarin first. Get Mandarin really good. Then Korean. Then English. The platform is built to deliver structured courses — but the first course has to be the marquee one, the one a learner could actually finish.
A planned Korean course that’s 5% built helps no one. A finished Mandarin course that someone can read end-to-end is a real product.
Same goes for features. Speaking practice was on the roadmap for three months before it shipped. It shipped late, but it shipped correctly — handling the China region case was load-bearing for it being usable, not a polish detail to defer.
What I’d tell myself in November
Three things, if I could send a postcard back to the version of me that started this.
Stop renaming things. A name is a fine thing to get wrong on day one. Re-renaming costs more than it’s worth most of the time. (I have done this exactly four times now.)
Buy the structured books. I spent weeks bouncing between vocabulary lists from forums. The HSK 3.0 standards are a real published document. Buying the structured books and following them is faster than reverse-engineering what they cover.
The audience is patient. I kept thinking I had to ship fast or people would lose interest. The opposite is true: language learners are by definition patient — they’re already committing to a year of practice. They don’t need a demo on day three. They need something that works on day three hundred.
What’s next
Mandarin keeps growing — Band 1 and Band 2 are live, Band 3 is being audited now. Korean course content authoring kicks off after Mandarin Band 3 ships. The platform side is calmer than it was six months ago, which means more time goes into the actual learning content — which is where it should be.
If you’re using Bookverse: thank you. If you’re not: open a chapter sometime. The first one is free.