The app was never the hard part. The hard part was everything the app assumed would just work.
Step one: stop asking, start listening
The old app asked the server “anything new?” every 10 seconds. The fix is to flip it around and have the server tell the app the moment something arrives.
On September 19 Leo shipped a web app version of the app built that way:
- Server-Sent Events keep one quiet connection open to my server, and a new text shows up on screen the instant it lands. No polling, so no redraws stomping on my typing.
- Web Push wakes the phone when the app is closed. Leo wrote that part from the spec and checked it against the official test vectors instead of trusting a library.
- A login per device that I can revoke one at a time, contact names, sharing photos into the app, and a Back button that actually goes back.
It passed 46 of 46 acceptance checks, and a real text from my main number to my 4040 line arrived in under six seconds.
“Never miss an alert”
Two days later I’d used it enough to know what was still wrong. I wanted different sounds for different people. I wanted reminders that keep nagging until I answer. The way I put it to Leo:
A web page can’t do that on Android. It can’t give each contact its own sound, and it can’t keep reminding you after you swipe it away. A real Android app can. So the design became a shell: a small native Android app that hosts the web app, plus the native pieces a web page can’t do:
- a background service that holds the live connection to my server,
- real Android notifications with reply right from the notification,
- per-contact sounds, reminders and quiet hours (that’s message 06).
September 23: one afternoon
On the 23rd I told Leo the app was still buggy. It still deleted what I was typing sometimes, it jumped around while scrolling, and Back exited the whole app. Leo stopped patching and rebuilt it.
At 2:34 PM it snapshotted the live code. Then three builder agents worked in parallel: one on the server, one on the web app, one on the Android shell, all working to one written contract for how the pieces talk.
A fake phone company for testing
You can’t test a texting app by spamming real texts all afternoon. So the rebuild came with a staging harness. It’s my real server code, wired to a fake Telnyx that never touches the network. Send a text and the fake carrier answers like the real API, then a second later plays back a “delivered” receipt through the same code path real receipts use. It can also inject fake incoming texts and fake picture messages on command.
Every screenshot of the app in this story comes from that harness, with made-up people in it.
What testing caught
The server review turned up 18 bugs in code that had been “working.” A few favorites:
- Vanishing texts. In a conversation over 500 messages long, the app loaded the oldest 500. New texts were in the database but never showed up.
- Lost-forever texts. The server marked an incoming webhook as handled before saving the message. If the save failed, Telnyx would retry, the server would say “already got it,” and the text was gone for good.
- “Retry” that didn’t. The retry button never actually re-sent anything.
- A freezing server. A few slow network calls blocked everything else while they waited.
- A sneaky picture. A picture message pretending to be a certain kind of image could have run code inside the app with one tap. Now only normal photo and video types open inline, and everything else downloads as a plain file.
Then putting it all together on real phones caught more. My favorite: the app and the server got into a “mark as read” argument that fired 647 times in two minutes. Another: Android never told the web page it was in the background, so the page kept marking texts read and cancelling my notifications while the phone sat in my pocket.
Shipped
By the end of the afternoon: 43 server tests, 72 web app tests, 28 of 28 checks on my Pixel and 27 of 28 automatic checks on my TCL (the last one, replying from the notification, I checked by hand). At 6:27 PM, version 0.2 went live on both phones, with a rollback copy saved in case it didn’t hold.
It held. So that night, we kept going.
