You already know what you need to say. Then it comes out soft.
A rehearsal app for difficult workplace conversations. The counterpart pushes back, your hedging underlines as you type, and when you finally make a clear point they go quiet for three real seconds.
Download on the App Store · Demo video (1:50) · Website · Privacy policy
Built for RevenueCat Shipaton 2026.
| Live app | App Store, iPhone, iOS 18.2+ |
| Demo video | 1 minute 50 seconds |
| Shared code | 89% — 4,633 of 5,198 lines in commonMain |
| Tests | 101 — 61 Kotlin, 40 Worker |
| Billing | RevenueCat KMP SDK, one entitlement, one file |
| Architecture | ARCHITECTURE.md |
| How it was built | BUILDING-WITH-CLAUDE.md |
| Categories entered | Ship Kotlin Everywhere · Design · HAMM · Influencer (Career Coaching) |
The three things worth looking at first:
coach/HedgeDetector.kt— deterministic hedge detection, phrase-level, no model callcounterpart.ts— the silence beat, earned rather than randombilling/Plus.kt— the entire RevenueCat surface
You pick a conversation you are dreading, or describe your own. The other person answers in character: defensive, tearful, charming, or simply unmoved. They do not let a vague opener slide.
Three things happen that do not happen in a normal chat app.
Your hedging underlines as you type. Just, maybe, I think, sorry to bother you, does that make sense? are marked the moment you write them, with a running count. Nothing is blocked. You just get to watch yourself flinch.
When you make a clear point, they say nothing. The typing indicator starts, then stops. Three real seconds of dead air. Most people fill it and talk themselves back out of the thing they just said.
Afterwards you are scored on four things, quoted back word for word: whether you named the specific thing, said what it cost, left them room to answer, and held the point under pushback.
Run the same conversation twice and it shows you the line that changed.
graph TB
subgraph device["iPhone / Android"]
UI["Compose Multiplatform UI<br/>10 screens, one implementation"]
subgraph common["commonMain — 4,633 lines, 89%"]
HD["HedgeDetector<br/>deterministic, no network"]
SL["SilenceLog<br/>offered vs filled"]
CS["CounterpartState<br/>clamped ±1 per turn"]
RC["RunComparison<br/>the line that changed"]
FT["FreeTier gate"]
PL["Plus.kt<br/>RevenueCat KMP"]
end
subgraph plat["expect / actual — 4 seams only"]
KV["KeyValueStore"]
LN["LocalNotifier"]
CK["Clock"]
SK["Store key"]
end
end
subgraph edge["Cloudflare Worker — TypeScript"]
AUTH["Auth + rate limit<br/>KV namespaces"]
SAFE["Safety<br/>input and output"]
CP["Counterpart state machine<br/>shouldOfferSilence()"]
PROV["llm/provider.ts<br/>single interface"]
end
GROQ["Groq<br/>gpt-oss-120b roleplay<br/>gpt-oss-20b scoring"]
STORE["App Store / Play<br/>StoreKit"]
RCB["RevenueCat<br/>entitlement 'plus'"]
UI --> HD
UI --> common
common --> plat
UI -- "HTTPS, transcript only" --> AUTH
AUTH --> SAFE --> CP --> PROV --> GROQ
CP -- "reply, state, silence" --> UI
PL <--> RCB
RCB <--> STORE
KV -. "device local only" .- DB[("Rehearsals, runs, scores<br/>never leave the phone")]
The rule the diagram encodes: anything that gets scored is computed deterministically, not asked of a model. Hedge counts and silence counts are produced on the device and sent to the scorer as ground truth, overriding whatever the model thinks it saw.
Full detail, including the request flow and the state machine, is in docs/ARCHITECTURE.md.
shared/src/commonMain/ 4,633 lines — all logic, all UI
coach/ HedgeDetector, SilenceLog, CounterpartState, RunComparison
billing/ Plus.kt — the only file importing RevenueCat
ui/ 10 Compose screens + theme
data/ DryRunApi (Ktor), DryRunStore
models/ Rehearsal, Turn, RunRecord, FeedbackReport
shared/src/iosMain/ 220 lines — 4 actuals + entry point
shared/src/androidMain/ 276 lines — 4 actuals
shared/src/commonTest/ 61 tests
iosApp/ 28 lines of Swift
androidApp/ 41 lines
worker/src/ Cloudflare Worker, 40 tests
Backend
cd worker
npm install
cp .dev.vars.example .dev.vars # add GROQ_API_KEY and MINT_SECRET
npm test # 40 tests
npx wrangler devAndroid
./gradlew :androidApp:installDebugiOS
open iosApp/iosApp.xcodeproj # then run on a simulator or deviceTests
./gradlew :shared:testAndroidHostTest # 61 Kotlin tests
cd worker && npm test # 40 Worker testsThere is no API key in this repository. The app runs against a deployed Worker that holds the only model credential; the client never sees one.
What you type is about a real colleague, sometimes by name. So:
- Rehearsals, runs and scores are stored on the device only
- Message content is sent to the Worker for the length of a request and is never written to any database
- There is no share button anywhere in the app. On purpose. A share sheet here is a privacy hazard dressed up as growth
- Delete everything on the home screen erases all of it, with no cloud copy to survive it
Full policy: PRIVACY.md.
The product decisions, the store work and the judgement calls are mine. Claude Code did most of the typing.
An honest account of the split, including three bugs that shipped anyway and how each was actually found, is in docs/BUILDING-WITH-CLAUDE.md.
MIT.