Molochou
← All work

Cercano

A messenger where the server is not a party to the conversation. iOS and Android, own crypto layer, self-hosted calls.

MessagingDesign & engineering2026Case studyFull visual case

Messages are encrypted on the device with a per-chat key and stay encrypted in transit and at rest. Calls run peer to peer through a TURN server the project operates itself rather than renting from someone else.

Year
2026
Role
Product design, design system, React Native front-end
Stack
React Native · Rust (Axum) · PostgreSQL · WebRTC
Status
In development, iOS & Android

Theproblem

Encryption has no interface. None of the guarantees this product makes are visible on screen, and a privacy app that feels like a privacy app gets deleted after a week. The work was building something that behaves like an ordinary messenger (fast, familiar, forgiving) while every promise underneath it is real.

WhatIdid

01

Signing in with nothing to steal

A phone number and a six digit code. No password to choose, no email to send a reset link to, no social login handing a third party the moment you arrive. On first launch the device generates its own X25519 keypair: the public half is registered so other people can reach you, and the private half never leaves the phone. There is no account sitting on a server waiting to be talked out of it, because the part that matters was never on a server.

02

A chat that gives nothing away

Reply with a quote, edit what you already sent, forward, copy, pin, delete, and six reactions on a long press. Delivery reads in three states, sent, delivered and read, and the typing indicator runs live over the socket. None of this is a security feature. It is the list of ordinary things whose absence is what makes somebody notice they are holding a security product, and a messenger nobody keeps open protects nobody.

03

One tap, everything gone

A red button on the chat list clears chats, media and local storage, signs the device out, and purges the account side at the same moment. There is no confirmation dialog, because the situation the button exists for is not one where you are asked twice. You choose in advance which of the three it takes, and what opens afterwards is the welcome screen, the same one you would meet on a phone that never had the app.

A second code, a different reality
04

A second code, a different reality

Set a decoy code alongside the real one. The real one opens the app as it is; the decoy opens it with an empty chat list and the real content out of reach. No hidden menu, no long press, no gesture that could give it away. To anyone reading over your shoulder it is an app you barely use, which is deniability that does not ask the person holding the phone to understand the word.

05

Voice messages that carry no waveform

Hold to record, release to send. The wave draws itself as you talk and again as it plays, at 1x, 1.25x, 1.5x or 2x. The shape is not recorded: it is derived from the message id, so every device draws the same picture without the waveform ever being sent or stored. The audio itself goes through the same encryption the text does.

06

Calls with nobody in the middle

Audio and video one to one, with picture in picture, camera flip, mute and speaker handled in the app rather than left to the system. Group calls take up to four people in a mesh, every participant connected directly to every other. The server knows who is in a call and nothing about what is in it, and when a network refuses a direct route the relay is a TURN server the project runs itself.

A closed circle
07

A closed circle

No directory, no username to search for, no strangers. People arrive by scanning your code or through a number already in your phone. Groups carry admin roles and a shared gallery, and group encryption uses the same per-chat key, encrypted separately for every member. A product with no public discovery has no spam problem to retrofit a defence against.

08

Messages with a shelf life

Auto-delete is set on a chat rather than once for the whole app: off, five minutes, an hour, a day, a week. The server drops what has expired on its own instead of waiting for a client to come back and ask, and it goes from the other phone at the same time. That last part is what makes it a feature rather than a tidy-up of your own screen.

Both themes, drawn rather than derived
09

Both themes, drawn rather than derived

Dark is true black, which an OLED panel does not light at all. Light inverts the bubbles, so outgoing becomes black on white. Both were drawn, neither generated from the other, and the app follows the system until told otherwise. Everything else is greyscale on purpose: red means danger and green means online, and there is no third colour to dilute either. Text scales at 1.0x, 1.15x and 1.3x through the app’s own component instead of the platform’s, so the layout decides how a third more text reflows.

An identity that survives 24 pixels
10

An identity that survives 24 pixels

The mark is a cat curled into a C, tested on black, dark grey, white and light grey, with minimum sizes defined down to 24 pixels and an app icon drawn for the same grid.

Underthehood

The cryptography is deliberately boring

Key exchange is X25519, messages are AES-256-GCM with a fresh nonce each, and a backup derives its key with PBKDF2 over SHA-256 at a hundred thousand iterations. All of it through @noble/curves, @noble/ciphers and @noble/hashes rather than an implementation written here. Interesting cryptography is how a product ends up with an interesting vulnerability.

A schema that cannot read its own rows

Two tables exist only to move keys around: one holds each user’s public key, the other holds the chat key encrypted separately for every member of that chat. The content column on messages holds ciphertext, so there is no query that returns readable text, including the ones somebody writes by accident.

The client was rebuilt from Flutter to React Native

The first client was a complete Flutter app: around sixty screens with its own encryption, socket, calling, notification and update services, down to a Dart implementation of binary patch application. The web target decided the migration: Flutter’s web build was not good enough for the web client this product needs, and maintaining a second separately written client alongside it cost more than the rewrite.

Rust on the server, and a socket for the live half

Axum on Tokio, with SQLx checking every query against the real schema at compile time. The socket carries what has to be immediate, delivery, typing, presence and call signalling, and HTTP carries the rest. A process that holds everybody’s ciphertext is the wrong place to find out about a memory bug.

Updates that do not wait for a store review

JavaScript ships over the air as a bundle; native changes ship as binary APK patches built with bsdiff, so users download the difference rather than the whole app. A messenger that cannot push a fix within hours has a security problem it cannot close.

Nine languages, designed for from the start

English, Russian, Spanish, German, French, Chinese, Japanese, Korean and Arabic live in i18next dictionaries, and the layout is built to survive text 40% longer than English as well as a language that runs the other way. Retrofitting that into a finished design means redrawing every screen.

Next projectDesign System AuditDesign tools