Jeff Mixon / Knit: mesh chat with no servers, no accounts, and no Google Play Services

Created Thu, 09 Jul 2026 00:00:00 -0700 Modified Thu, 09 Jul 2026 23:02:23 -0700

The premise

Most chat apps assume a server. Knit doesn’t. It works with no internet connection, no cell service, and no account, because it talks directly to the phones physically near it over Wi-Fi Aware and Bluetooth LE, and those phones relay for each other. The more people nearby running Knit, the further a message travels: across a crowd, a building, or a campsite, well past the sender’s own radio range.

That premise held from the first version. What changed in the rewrite is what the app leans on to make it true. Nearby Connections, the Google Play Services API that used to handle peer discovery and radio work, is gone. Knit now drives Wi-Fi Aware and Bluetooth directly, with no GMS involved at all, which means it meshes on de-Googled and GMS-stripped phones too, not just the ones Google’s stack happens to support. The trade is that I now own all the radio code Nearby Connections used to hide from me. That’s more code to get right, but it’s also why the app runs on phones Google’s stack refuses to touch at all.

Where it’s built to work

That’s also the product, not just the architecture. Knit is built for the moment an ordinary messaging app goes quiet:

  • A festival or concert where the crowd chokes the cell towers
  • A flight, a ship, or a stretch of highway with no signal at all
  • A campsite or trailhead past the last bar of LTE
  • A power outage or an emergency where the network itself is down
  • Or just wanting to reach the people standing near you without routing through a data center on another continent

None of that works if the app depends on infrastructure that isn’t there. So it doesn’t.

Stack at a glance

  • UI: Jetpack Compose (Material 3), Kotlin 2.4.0, SDK 36, JDK 21
  • Radios: Wi-Fi Aware (NAN) and Bluetooth LE, run simultaneously behind one MeshTransport seam, driven directly through android.net.wifi.aware.* / android.bluetooth.*, no Google Nearby
  • Wire format: kotlinx.serialization CBOR, a layered, opaque-bytestring envelope
  • Crypto: Google Tink (HPKE/X25519 key wrap, AES-256-GCM, Ed25519 signatures), hardware-backed identity keys in AndroidKeyStore
  • Persistence: Room over SQLCipher, encrypted at rest
  • Moderation: bundled TFLite/LiteRT classifiers for abusive text and explicit images, fully offline; the app carries no INTERNET permission
  • Release: R8 full-mode shrink, optimize, and obfuscate

Two radios, driven directly

Nearby Connections used to abstract away which radio it picked underneath you. Driving Wi-Fi Aware and Bluetooth directly means Knit decides, and owns everything that follows from that decision. A MeshTransport interface has two implementations running at once, one per radio, merged behind a CompositeMeshTransport so a phone with only one of the two radios still meshes over it. Bluetooth LE also covers the older devices that don’t have Wi-Fi Aware hardware, which is why minSdk could come back down to Android 10 after briefly requiring 13 partway through the rewrite. The goal is to reach as many phones as possible, not the newest ones.

Holding a mesh of more than two phones together took a real design change: instead of standing up a fresh Wi-Fi Aware responder for every peer, each node runs a single shared responder that accepts a connection from anyone, and the connecting phone identifies itself as the first thing it sends once the socket is open. Say who you are after connecting, not before. It’s a small inversion, but it’s the one that lets a mesh grow past a pair of phones into an actual crowd.

Staying reachable while the phone’s in your pocket

A mesh node is only useful when it’s actually there for a message that arrives, and Android does not want an app running forever in the background. The compromise is a foreground service that keeps the radios alive behind a persistent notification, tied to a power-aware duty cycle. Advertising stays on nearly all the time, since a phone in someone’s pocket should still relay for whoever’s near it. Scanning, the expensive part, runs in short bursts when the screen is on or the phone is charging, and backs off hard when it’s idle on battery. A motion-sensor wake-up and a periodic heartbeat cover the rest: check in when the phone starts moving into new surroundings, and check in anyway once enough time has passed without one. The result spends real power on the move and almost none sitting still, without ever going dark.

A wire format built to carry a signature

The wire protocol is kotlinx.serialization CBOR in layers. The outer WireEnvelope is the only part a relay ever touches, and the only thing it’s allowed to change is the hop count. The payload underneath is two opaque byte strings that pass through every hop byte-for-byte, which matters because that’s also what’s signed: one Ed25519 signature covers every frame the mesh carries (chat, profile, reactions, receipts, all of it), verified byte-exact against what was actually sent. Nothing in the mesh re-encodes it. A new field is nullable or defaulted, a new message type is just a string an old build doesn’t recognize yet, and none of it requires a schema migration. That’s the part I care about most in a mesh where you can’t force every phone in range onto the same build.

Private by design

Direct messages and multi-member group chats, with quoted replies, reactions, and @-mentions, are end-to-end encrypted: Tink’s HPKE wraps a per-message key to each recipient over X25519, bodies are sealed with AES-256-GCM, and identity keys are generated on-device and held in hardware-backed AndroidKeystore. Meeting someone in person is when trust is cheapest, so Knit leans on that: a safety number or a QR code confirms you’re actually talking to who you think you are. The one deliberate exception is the public “Nearby” room, which stays plaintext, because a broadcast to everyone in range has no fixed recipient set to encrypt toward. (The full threat model, including why there’s no forward secrecy, gets its own post.)

Delivered even when nobody’s there to receive it

Two phones that only cross paths for thirty seconds still need to catch each other up. Knit’s store-and-forward layer uses content-digest anti-entropy to do that: a phone holds onto traffic it’s carrying for someone else, hands over whatever a newly met neighbor is missing, and an idle mesh with nothing to sync does zero data-path work in the meantime. Message requests and a handful of anti-Sybil limits, retention caps, and a bound on how far a claimed timestamp can lead the local clock, mean a flood of unsolicited contact attempts costs the app nothing it can’t afford.

Moderation with no server to send anything to

Every messaging app eventually has to answer for what a stranger sends a stranger. Knit can’t phone anything home to answer it: there is no INTERNET permission in the manifest, period. So the lexical profanity filter and the bundled TFLite classifiers for abusive text and explicit images all run entirely on-device. Flagged content gets blurred behind a tap instead of blocked outright, and blocking a sender is always one tap away.

Getting Knit to a phone that doesn’t have it

The last mile of a serverless app is the app store itself, which is its own kind of infrastructure dependency. Knit sidesteps it too: it shares itself to a nearby phone over the same mesh, without a store and without an internet connection, by merging its installed split APKs into one universal APK and re-signing it on-device.

Shipped obfuscated

Release builds run R8 in full mode: shrink, optimize, and obfuscate. That’s safe to do because the wire format’s map keys come from compiler-generated serializer descriptors baked in at compile time, not reflection over property names at runtime, so renaming the classes around it is byte-safe by construction.

Closer

A “no infrastructure” chat app forces every problem onto the device: no backend to lean on, and now, no Google Play services layer either. Discovery, encryption, moderation, storage, even installing the app in the first place, all of it runs on code that ships in the APK, on hardware you could debug with a laptop and a second phone.

Knit is free, funded by tips, and live at getknit.app. For the parts this post didn’t cover, the threat model, the flooding protocol there’s the companion write-up.