Course Kit
Zero to 370+ paying users, alone.
A coach sells lessons in blocks of sessions. What happens next is everything I designed, built — and got wrong once.



Screens and figures are sample data
The problem
A freelance coach sells lessons in blocks. What is left of a block lives on a paper card, in a chat thread, or in a spreadsheet only the coach can open. The client has no way to check the same number.
Cards get lost in wash · Spreadsheets out of sync · Awkward session-end disputes.
One shared record on both devices · Coach deducts, student verifies immediately · Zero friction.
What I built
One record, open from both ends. The coach deducts a session; the client sees the same balance and the same history a moment later. Neither side keeps a private tally, so there is nothing left to disagree about.
Around that record: a Flutter app on iOS and Android, a web storefront, three payment rails, and a Firebase backend on GCP. All of it mine.





Decisions
Flutter & Firebase for 1-person scale
Two native apps plus a custom backend is three full-time jobs, and there was only one of me. So I cheated smart: one Flutter codebase for both app stores, and managed GCP services that keep the lights on while I sleep.
One-time purchase over subscription
Avoided recurring dunning, billing state machines, and churn complexity upfront to validate willingness-to-pay on release one.
Local-first vs cloud sync boundary
Self-recorded lessons live offline on phone (zero cloud cost); collaborative courses sync via Firestore accounts.
The interesting engineering
Selling on three platforms means being paid in three ways. Apple, Google and Stripe report a purchase in three different shapes. Cloud Functions normalise them into one Firestore entitlement schema, so the client never learns where the money came from.
Conflicts resolve to the highest tier, not the last write.
Every interface change was additive, so the three-tier pricing change shipped with zero regressions.
Results
A growing base of paying users acquired purely through organic SEO with zero paid advertising.
Architectural Evolution & Post-Mortem
Here is where I got it wrong the first time: I drew my data boundaries by feature, not by plan. Then self-recording users asked for multi-device sync, and that one decision cost me a 2.5-week migration. An expensive lesson — but you only need to learn it once.
| Architecture Version | Data Boundary Design | Migration Friction | Key Engineering Lesson |
|---|---|---|---|
| V1: Feature Boundary | Offline SQLite for self-records; Cloud Firestore for collaborative courses. | 2.5 Weeks Migration to move local records to cloud when adding sync. | Drawing boundaries on features causes painful schema forks across tiers. |
| V2: Plan Boundary (Refactored) | Unified account model with entitlement flags controlling cloud sync vs local storage. | Zero Migration Cost when users upgrade plans. | Architecture should scale by user entitlement tiers, not feature branches. |