Case 01
InfoPhone
- 2025
- ongoing
- Product Manager (& engineer of the prior build)
- Native iOS (Swift) + Native Android (Kotlin) + raw ejabberd + LiveKit + NestJS + Drizzle
- In testing phase. Not yet shipped to end users.
Real-time chat + voice/video at Enso Webworks. We shipped a Flutter + MirrorFly build, used it in production, and made the call to scrap it and rebuild native — raw ejabberd for chat, LiveKit for calls, NestJS for the rest. This case is about killing a working build before sunk cost killed the product.
Problem
The issue was not one bad bug; it was cumulative drag. Every workaround added more velocity tax, while the product still missed features users expect from a modern communication app.
- Performance issues across multiple surfaces. The core product promise was real-time communication, and the old build made that promise feel unreliable. Frequent crashes and laggy calls made users distrust the entire app, even when the rest of the product worked.
- MirrorFly SDK opacity. We were locked into a vendor whose code we couldn't read. Every bug routed through their support queue; basic protocol questions cost us weeks. Speed of iteration depends on access to your own stack, and we didn't have it.
- Closed SDK = blocked features. The roadmap was being shaped by vendor support, not user needs. Features that should have been table stakes stayed blocked, creating competitive gaps we could explain internally but not justify to users.
- Flutter ceiling at iOS PiP. PiP is expected behavior for a serious calling product in 2025. Missing it made the product feel dated, and another Flutter workaround would not have been a credible promise.
- Wrapper-on-native architecture. Every feature carried a double implementation tax. The team was moving slower while still not getting the benefits of a fully native product or a clean cross-platform one.
Options
Bet
Choose ownership over convenience: one expensive rebuild now, instead of letting vendor opacity and platform compromises tax every future feature.
Why this stack at all — code ownership
The call was operational confidence. During an incident, the team should debug by reading code and logs, not by waiting for a vendor reply.
Why native specifically
The product should not negotiate with its framework for basic communication features. Native keeps future UX decisions open instead of pre-deciding what is impossible.
Why LiveKit specifically
- Enso already had operational knowledge from another LiveKit app, so adoption risk was lower than a cold vendor switch.
- Choosing LiveKit kept the door open for future provider swaps instead of betting the product on one external roadmap.
Why raw ejabberd over a wrapper
For chat, long-term stewardship mattered more than SDK convenience. The product needed a stack we could own for years, not a vendor shortcut we might outgrow again.
Outcome
The product is in testing, not shipped to end users yet. The important milestone is that the team has crossed from patching the old build to validating the new one.
Features that were previously blocked are now in scope. The team can make product commitments based on capability, not vendor limitation.
Reflection
product — ownership
I killed the old build later than I should have. Every patch sprint made the sunk cost heavier, but the signals were already clear. I should have written the kill memo two months sooner.
engineering — process learning
I should have forced SDK proof earlier: PiP, screen share, backup/restore, source-level debugging, and failure-mode ownership should have been week-one POC gates before the team built on top of MirrorFly.