Inspiration
I ruminate. If I have a hard conversation coming up I run it in my head for days, and afterwards I replay it and find the things I never got to say. When I read Heather's brief, "a flight simulator for hard conversations", that was the part that landed. I didn't need more advice. I needed somewhere to rehearse the moment, to say the words, hear the pushback, and try again before the real conversation.
The other thing that shaped it came out of researching copy. People are already doing this with ChatGPT, Gemini, Claude, etc in voice mode, telling it to interrupt them the way a stressed manager would. That told me the behaviour exists. It also told me the competitor was free and already installed, so the app had to do something typing at a chatbot doesn't.
What it does
You tell FirstSpar about the conversation you're dreading: who it's with, what's been happening, how they tend to react, what you need out of it. It builds a counterpart from that and you talk to them. Out loud, over the phone's mic, in real time. They push back, get defensive, go quiet, ask the question you weren't ready for. You can interrupt them and they stop mid-sentence.
When you end the spar you get a debrief: scores on clarity, empathy, specificity, holding the line and composure, what worked quoted from your own transcript, and one thing to try next time. It never rewrites what you said into a better script. That was Heather's rule and it became a hard constraint in code. The debrief schema has no field for a suggested line, and the parser rejects a response that contains one.
There's a library of over twenty scenarios across feedback, boundaries, saying no, making the ask and hard news, plus the ones you build. Three difficulties. A before-and-after confidence rating on every spar so Progress can show whether it's actually working, from the first spar. A Skill Map that reads the transcript for durable skills, where silence is never a low score. And if you say something that isn't roleplay, the counterpart drops the persona and the app takes you to a support screen with real resources.
Free gets you 3 spars per week at Practice difficulty. Pro, through RevenueCat, unlocks Realistic and Pressure Test, longer sessions and building more conversations of your own.
How I built it
Flutter on iOS. Gemini Live through Firebase AI Logic, with native audio in and out. Firestore for data, Remote Config for the scenario library, Firebase Auth with anonymous sign-in that can later upgrade to Apple or Google, and RevenueCat for subscriptions. Built solo with Claude Code.
The first two days were a spike to prove the voice loop worked on a physical phone before I committed to building the rest.
After that I wrote a build spec: 51 numbered requirements, each with a test target and a reason for existing. It’s around 700 lines now. Every odd-looking piece of code carries a REQ number pointing back to the decision behind it. When the product changed, I edited the spec instead of deleting the old reasoning, so the history survived with the code.
The architecture rule I stuck to was simple: anything the app claims about a person should be a pure Dart function with its own test file. What to practise next, whether you’re ready, and the patterns you show across conversations.
State lives in Cubits. Tests use in-memory fakes instead of mocks. There are around 700 tests now. First commit: August 20. Current build: 11.
Challenges I ran into
Echo. On loudspeaker, the mic could hear the model’s own voice. Gemini’s voice detection interpreted that as the user speaking, so the counterpart would interrupt itself a second into almost every reply.
iOS echo cancellation couldn’t solve it because playback was running through a different audio unit. The fix was a local level gate on the mic stream. The important detail was gating until playback had actually drained, not until the model reported the turn complete. Gemini stopped generating about a second before the audio finished playing, and that tail was enough to trigger another interruption.
The safety exit. I wanted the counterpart to drop character if someone said something that needed a real-world response. Prompt instructions weren’t reliable enough. Asked to leave the persona quietly, the model would say things like “I’m not Sam anymore.” Ban that phrase and it would find another version.
What finally worked was a function call that the app treats as “end the session.” That turn is removed rather than answered in character.
Teardown. Closing a live session in the wrong order took 234 seconds. Closing it in the right order took 399 milliseconds.
App Review, four times. Build 2 was rejected because my microphone screen had a button that said “Allow microphone access.” Apple considered that the app answering the system permission prompt on the user’s behalf.
Build 4 exposed another problem: the denied state had an “Open Settings” button with no way back. Build 5 followed Apple’s own suggestion and used a dialog with a Settings link, and that was rejected on similar grounds.
The rule I ended up with was simple: explain why the microphone is needed before the system prompt. If permission is refused, state the device’s current permission state and stop. No instructions, no Settings button.
Build 7 was about privacy. The privacy policy named Google as the processor, but Apple required the disclosure inside the app before any data was sent. The app now explains what is sent, who receives it, and asks for explicit consent before the first question.
Account deletion that didn't delete. I deleted the Firebase user first and the user’s data second. That destroyed the auth token the security rules needed, so every delete after the account removal was denied. The errors were swallowed and the app reported success anyway. Ten unit tests passed because the fake implementation didn’t enforce that precondition. Testing on a real phone found it immediately.
Fonts. The entire app shipped in regular weight for weeks. The google_fonts package had the weight baked into the family name, so applying a weight afterward was silently ignored. I ended up bundling the font instead.
Accomplishments that I'm proud of
- It works on a phone, on speaker, with interruption, at around 350 ms of latency. That was the original bet.
- The debrief quotes your best moment back to you without writing a script for you.
- Nothing is locked behind sign-in. You can go from welcome to your first spar without creating an account, and your work follows you if you sign in later.
- The safety exit. It was the right thing to build, and much harder than it looked.
- I’ve used FirstSpar for a real conversation. The actual conversation didn’t play out the way the spar did, and that was the point. I walked in knowing what kinds of reactions and pushback I might face. When the moment came, I felt prepared enough to stay in the conversation, say what I needed to say, and not leave wishing I’d said the important part.
What I learned
- Unit tests can’t catch a correct function that nobody actually calls. Test at the point of use.
- Negative instructions are weak against a model’s reflexes. When something really matters, give the model a tool instead.
- Write copy in the customer’s words. I kept a voice-of-customer file throughout the build, and some of the strongest language in FirstSpar came directly from how people described the problem themselves.
What's next for FirstSpar
- Consider deploying FirstSpar to the Google Play Store.
- Pricing. It’s the biggest untested assumption in the entire plan. Now I need to find out whether people will actually pay for this.
Log in or sign up for Devpost to join the conversation.