All case studies

Mobile App case study

Hush

Our own product. Anonymous, ephemeral one-to-one chat in Flutter, with end-to-end encrypted messages and calls and no account to create.

Hush interface
The Problem

What was actually broken

Hush is our own product rather than a client engagement. It started from a narrow question: how much of a messaging app can you remove before it stops working?

Every mainstream chat app begins by asking who you are. A phone number, an email, a profile, a contact list. That identity is the product, because it is what makes the network valuable and what makes the account recoverable. It is also the thing you cannot take back once you have handed it over.

So the constraint we set was no accounts at all. No sign-up, no phone number, no profile, no contact list, nothing to recover because there is nothing stored. Two people should be able to open the app, exchange a six-character code, talk, and leave nothing behind.

That constraint makes almost everything harder. With no accounts there is no identity to attach a key to, which is exactly the situation a man-in-the-middle attack likes. With no server-side history there is no message list to page through. With no login there is still a device that has to be authenticated somehow, or the database is open to anyone.

Our Approach

The decision that shaped everything else

The code is the entire onboarding

On open, the device signs in anonymously in the background and gets a short code. Share it, and the moment the other person types it in, a private session opens between the two of you. There is no screen asking for a name, because there is no name. End the session, from either side, and the conversation is deleted for both.

Encrypt on the device, so the server only ever holds ciphertext

Message content is encrypted before it leaves the phone. Firestore stores scrambled bytes it cannot read, which means the security promise does not depend on trusting our own backend. It also means the security rules never need to inspect content, and can concentrate on the thing encryption cannot defend by itself.

The attack encryption cannot stop on its own

If anyone can overwrite the public key you published during the handshake, they can substitute their own and sit in the middle reading everything, and the maths will not notice. The Firestore rules exist to close that: nobody can overwrite someone else’s published key, the host identity cannot be rewritten, a guest seat once taken is fixed, and a message’s sender must match the writer’s own uid. Those rules ship in the app repository because they are part of the security model, not configuration.

Give people something to compare

Cryptographic guarantees are worth little if the two people cannot check them. Both sides see a safety code made of symbols they can read out loud to each other. It started at five symbols and moved to seven, because five is short enough to be worth brute-forcing and seven is not.

The Build

What we shipped, and what fought back

Flutter and Firebase, Android and web from one codebase

Flutter 3.41 on Dart 3.11, with Firebase anonymous authentication and Cloud Firestore for the session and message documents. GetX handles state. One codebase produces the Android build and the web build, which is the argument for Flutter on a product this size: the alternative was writing the pairing and crypto flow twice.

Ordering from the server, not the handset

Messages are ordered by server timestamp rather than device clock. Two phones with slightly different times will otherwise interleave a conversation wrongly, and it looks like a bug in the app rather than a bug in the clock. Delivery and read ticks and the live typing indicator run off the same document stream.

Calls that connect across networks and stay encrypted doing it

Voice runs over WebRTC. Two phones on different networks often cannot open a direct path, so calls fall back to a relay when they need to. The audio stays end-to-end encrypted through that relay: it forwards scrambled packets and cannot listen. Calls default to loudspeaker on connect, and a call that cannot connect says so within twenty-five seconds instead of ringing into nothing.

Closing the gaps around the edges

Android backup is disabled for the app’s private data, so the anonymous identity cannot be lifted off a device backup. A stale call can no longer ghost-ring when you reopen a chat. Nearby Connections handles pairing when two devices are physically together. Small things, but each one was a way the promise leaked.

The Result

The numbers it holds

Hush ships as an Android APK and a web build from one Flutter codebase, currently at version 1.4.2.

The part we carry into client work is the discipline of writing down what a product promises and then hunting for every place the promise leaks. Encryption on the wire means nothing if the key exchange can be substituted. A deleted conversation means nothing if a device backup keeps a copy. A safety code means nothing if it is short enough to fake. None of those are cryptography problems, they are product problems, and they only surface if you go looking.

It is also our own product, so nothing here is a claim about a client we cannot name.

Built with
FlutterDartFirebase AuthCloud FirestoreWebRTCGetXAndroid

Hush is the clearest example of how we build for phones: one Flutter codebase, real platform integration, and a security model written down rather than assumed. That work sits under our mobile app development service, where the scope, timeline and starting price are published before you talk to anyone.

Next case study

Content Studio

Tell us what’s slowing you down.

Fixed scope, quoted before kickoff, four to eight weeks to production. You talk to the engineer who writes your code.

Start a project