The idea
Every messenger I use is built to grow. More contacts, more groups, more channels, more people who can reach you. That's great for a phonebook. It's not what I want for the one conversation that matters most.
Two in the Bush goes the other way. It's an end-to-end encrypted messenger for exactly two people, and it can never hold a third. No contacts list, no groups, no feed, no discovery. You pair with your one person, and that's the whole app. The name comes from "a bird in the hand is worth two in the bush".
Leaving a second contact out isn't a missing feature, it's the product. Signal is a phonebook; this is a locket. Unpairing is deliberate and destructive: the shared history is erased on both phones, and you start from nothing.
It's live now: Two in the Bush on the App Store, free, for iPhone. The website is twointhebush.app.
Pairing without trusting the server
The hard problem in any end-to-end messenger is the key exchange. If the server hands each phone the other's public key, a malicious (or compromised) server can hand over its own keys instead and quietly read everything. Most apps solve this with safety numbers or QR codes you're supposed to compare. Almost nobody does.
I wanted verification that two people would actually do, at any distance. So pairing ends with the socialist millionaire protocol (SMP), the same idea OTR used:
- One of you creates a short pairing code and tells the other in person or on a call.
- The phones exchange public keys through the server.
- One of you picks a question only the two of you can answer - "what did we eat on our first date?" - and you both type the answer.
SMP proves you both typed the same answer without either phone revealing it, and the proof is bound to the exchanged keys. If anyone swapped keys in the middle, the check fails, even if they relayed every message perfectly. You get exactly one attempt; a wrong answer tears the pairing down and you start again with a new code. An attacker gets one blind guess, and failure is loud.
The nice part is the human factor. Comparing 60 digits feels like homework. Answering a question about your own history feels like sealing the locket, which is what it is.
I wrote the SMP implementation twice, once in Go and once in Swift, and pinned them together with shared test vectors so both produce byte-identical results.
What the server sees
As little as I could manage:
- Messages: each one is sealed on the phone with a fresh one-time key (X25519 + ChaCha20-Poly1305 via Apple's CryptoKit). The server relays and queues ciphertext it can't read, and deletes it as soon as it's delivered.
- Photos, voice and video: encrypted on the phone before upload, deleted from the server after the other phone downloads them.
- Notifications: always the same fixed "New message". No text, no sender. Apple's push service never sees anything readable.
- Sign-in: Sign in with Apple with no name and no email requested. The account is an opaque ID.
- On the phone: your history lives in an encrypted store. By default it's protected by the device's own lock screen; you can add a passphrase on top, and ten wrong guesses erase the conversation from that phone.
The backend is deliberately boring, the same shape I use everywhere: Go, PostgreSQL and Caddy in one Docker Compose file on a small VPS in the EU. When all it does is shuffle ciphertext, one small server goes a very long way.
Three rejections and a lot of waiting
I submitted the app on 13 July. It went live on 13 October. Three months, for an app that was working on my phone before I ever pressed Submit.
Almost none of that time was work. Every fix took me a day, sometimes an afternoon. Then the new build went back in the queue, and I waited. And waited. For weeks:
| Submitted | Verdict | Wait |
|---|---|---|
| 13 July | Rejected 21 July | 8 days |
| Fixed the listing the same week | Rejected 6 August | 2 more weeks |
| 7 August | Rejected 1 September | 25 days |
| 15 September | Approved in October | almost 4 weeks |
That waiting was honestly the worst part of the whole project. Not the crypto, not the Swift, not the server. You can't speed it up, you can't ask anyone, and there's no progress bar - just "Waiting for Review" sitting there day after day. You stop wanting to touch the code, because any change might mean another trip to the back of the line. And when the answer finally arrives, it's often about one screen, and the fix is twenty minutes of work... followed by another month of silence.
If you're planning your first App Store launch: budget for the queue, not the code. Assume every round trip costs weeks, and make each submission count by fixing everything you can see, not just what they flagged.
Each round did teach me something, though.
Round 1: one word. The listing used "intimate", which a reviewer read as adult-adjacent marketing. A metadata-only fix, no new build. Lesson: your keywords are read by a human with a checklist, not by your target user.
Round 2: three issues at once. - A one-to-one chat still counts as user-generated content. Even with no strangers, Apple wants a way to report and block, and terms you accept up front. Fair enough: I added all three, plus a small moderation tool to act on reports. - Account deletion "missing". It existed, but it lived in Settings inside the chat screen, and a reviewer testing alone can never pair, so they never saw it. A legally required flow, hidden behind a state the reviewer couldn't reach. Settings is now reachable before pairing too. - A password after Sign in with Apple. The local encryption password looked to Apple like a second sign-in.
Round 3: the same password, renamed. I'd reframed it as an "encryption passphrase" and explained the crypto in the review notes. Rejected again. Apple's objection was never about the word; it was about a mandatory screen after sign-in. The fix was to make the passphrase optional: skip it and the device lock screen protects your messages; add one later from Settings if you want the extra layer. Approved.
The pattern across all three: App Review reacts to what a reviewer experiences, not to what your notes explain. If they can't reach it, it doesn't exist. If they're blocked by it, the name doesn't matter.
The launch wasn't the finish line
I pressed Release and the app appeared in zero countries. Two separate switches, neither obvious:
- App Availability has to be set up on its own. The pricing page listing 175 countries is not the same thing.
- EU storefronts stay blocked until you declare your trader status under the Digital Services Act, even for a free app.
Both sorted the same day. Then there's the lag: the App Store page appears in a country before Apple's own lookup API admits the app exists.
What's next
Android is in progress, using the same crypto and protocol so an iPhone and an Android phone can share a locket. Next up for iPhone is optional message previews in notifications, decrypted on the phone itself and off by default.
If there's one person you'd want a private line to, give it a try. It's free, and it will never ask you to add a third.