Group chat that works when cell service doesn't.
Encore is an offline-first, Bluetooth-mesh group-chat app for live events — concerts, festivals, anywhere a packed crowd kills the cell network. It rides a proven BLE-mesh + Noise stack (vendored from bitchat) and wraps it in a deliberately friendly, themeable UI plus an event-pack layer that lets an organizer re-skin and configure the whole app for their event — with zero servers, zero accounts, and zero internet. Ever.
That swap is the core idea: with no pack you get Encore's deliberately minimal brand; the moment a signed event pack loads, the event's look and configuration take over every screen.
Encore is two native apps — iOS (SwiftUI) and Android (Jetpack Compose) — that share a wire format and interoperate on a single Bluetooth mesh. Phones near each other form an ad-hoc network and relay each other's messages hop by hop, so a crowd becomes its own infrastructure. No tower, no Wi-Fi, no backend.
On top of that mesh, Encore adds two things bitchat never tried to be:
- A friendly, minimal UI designed for someone holding a drink in a dark venue — big near-white buttons, honest delivery states, no jargon.
- Event packs — an organizer-signed bundle of branding, channels, an offline schedule, and announcement keys, distributed as a single QR code on a poster at the gate.
Cell service dies at packed venues — everyone knows the feeling of "I'll meet you by the left bar" turning into 45 minutes of no signal. bitchat proved that BLE-mesh chat genuinely works in a crowd. But its UI is hacker-IRC, and there's no story for the people actually running the event. Encore is the missing layer: the friendly, themeable, organizer-aware app built on the part that already works. We didn't reimplement the radio — we vendored it and built a product around it.
- Zero-account onboarding. One screen, no phone number, no email. Encore auto-generates a fun display name (reroll with a dice button) and that name is the only thing ever shared — and only over Bluetooth with people near you.
- Packless Nearby chat (the anti-bitchat home). With no event loaded, you still get a working public Nearby room ("everyone in Bluetooth range"), usable as a friendly local mesh chat anywhere.
- Scan to join an event. Point the camera at the organizer's
ENCORE:QR on a poster and the join preview appears. No event at the gate? Join via people nearby pulls the pack over the mesh from anyone who already has it. - Verification ceremony before you trust a pack. The join preview renders the event in its own colors and shows a 4-emoji fingerprint (e.g. 💜🌊🐙🌵) to compare against the venue screens — an out-of-band check that you're joining the real event, not a fake poster.
- Verified organizer announcements. Only Ed25519-signed announcements get the pinned "OFFICIAL" backstage-pass banner with a verified check. Tap it to jump to the broadcast channel.
- Private crew threads (group chats). Create a named group ("Left Bar Gang"), then invite people with a scannable QR or a spoken-aloud 6-letter code — a nearby member's phone hands over the key over an encrypted link.
- Friends & 1:1 DMs. Add friends by tapping them or scanning their personal QR (which pins their Noise identity). DMs are pairwise end-to-end encrypted, with honest delivery states and store-and-forward when a friend drifts out of range. Friends persist across events.
- Honest delivery, no fake confidence. Broadcasts show real reach ("reach: 6 nearby" = phones directly handed your packet), not invented double-checkmarks.
- One QR is your entire deployment. An event pack is a signed, self-contained config blob — branding, channels, schedule, announcement keys — distributed as a single
ENCORE:QR or shared over the mesh. No server to stand up, no app to publish. - Whole-app theming, zero assets. Five anchor colors plus an emoji/short-text monogram re-skin the entire app to your event. No image uploads.
- Read-only broadcast channels. Mark a channel
broadcast(e.g. Announcements) and only your keys can post — attendees see it read-only with a megaphone and an "OFFICIAL" pill. - Cryptographically verifiable announcements. Announcements are signed by your event's Ed25519 root key, so attendees' phones can prove a message really came from you — and only verified ones get the trusted banner treatment.
- Offline schedule built in. Ship the set times inside the pack; they work with no signal.
![]() |
![]() |
![]() |
![]() |
| Onboarding — no accounts | Join preview + fingerprint | Verified announcement | Honest "reach: N nearby" |
Three ideas, each understandable on its own.
1. The mesh. Every phone runs as both a Bluetooth Central (scanning) and Peripheral (advertising). When packets fly, each phone decrements a hop counter (TTL, default 7) and re-floods to its neighbors, with dedup. A crowd thus relays messages across distances no single radio could reach. Out of range for a while? A store-and-forward sync lets you catch up from a neighbor's cache. Pairwise DMs use the Noise XX handshake (Curve25519 + ChaCha20-Poly1305); public/Nearby chat is unencrypted and visible to anyone in range.
2. Event packs. An event pack is canonical CBOR describing the event (id, name, venue, window), a theme (colors + monogram), channels, the schedule, and the organizer's keys — Ed25519-signed by the event root key, compressed, base45-encoded, and prefixed with ENCORE: so it fits densely in a QR. Its identity is a SHA-256 packHash, surfaced as that 4-emoji fingerprint for human verification. See the event-pack spec and the golden vectors.
3. The 0xE0 interop trick. Encore's app frames ride the vanilla bitchat wire format as a custom packet type 0xE0 carrying canonical CBOR {"t": subtype, "b": body} (subtypes: chan, ann, gc, pack). The clever part: vanilla bitchat phones relay 0xE0 packets un-parsed — they don't understand them, but they still flood them. So every bitchat user in the crowd silently boosts Encore's relay density for free, and Encore phones interoperate on the exact same mesh.
For the full byte-level details, read docs/PROTOCOL.md. For how the apps are put together, read docs/ARCHITECTURE.md.
Encore is deliberately minimal: a quiet near-black canvas, near-white primary buttons, and exactly one muted indigo accent used sparingly. No gradients, ever. No emoji in the chrome (SF Symbols on iOS, material-icons-extended on Android) — the only intentional emoji are the 4-emoji pack fingerprint and the organizer's monogram. Delivery states are spelled out honestly ("reached N nearby", "delivered", "failed") instead of fake confidence. The default brand is intentionally restrained so that when an event pack loads, the event's palette can take over completely and the app feels like the event's own app.
bitchat-shit/
├── ios/ # iOS app + Swift packages
│ ├── App/ # The SwiftUI app (bundle id fm.encore.app)
│ ├── Sources/
│ │ ├── EncoreMesh/ # Vendored bitchat BLE-mesh + Noise stack (Unlicense)
│ │ └── EncoreKit/ # Encore domain layer: event packs, CBOR, channels, announcements
│ └── Tests/ # swift test — 32 vector/codec tests
├── android/ # Android app + Gradle modules
│ ├── app/ # The Compose app (applicationId chat.encore, com.encore.app)
│ ├── core/ # encore-core: pure-JVM event-pack parse/verify (com.encore.core)
│ └── mesh/ # Vendored bitchat-android BLE mesh (:mesh, GPL-3.0)
├── packtool/ # Organizer web app for building/signing packs (work in progress)
├── spec/ # Cross-platform wire spec + golden vectors
│ ├── EVENT-PACK.md
│ └── vectors/pack-v0.json
└── docs/ # PRODUCT, ARCHITECTURE, BUILDING, screenshots
Pure offline — no API keys, no network calls anywhere in the build. Both apps build green today. Full details (prerequisites, exact destinations, install commands) live in docs/BUILDING.md.
cd ios
swift build # compiles EncoreMesh (vendored) + EncoreKit
swift test # 32 tests: event-pack vectors, canonical CBOR, base45, announcements
cd App
~/.local/bin/xcodegen generate # writes Encore.xcodeproj from project.yml (not committed)
xcodebuild -project Encore.xcodeproj -scheme Encore \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' buildRequires Xcode 15.2+ with the iOS 17.2 simulator and XcodeGen.
cd android
./gradlew :core:test # 52 pure-JVM tests: the same vectors as iOS
./gradlew :app:assembleDebug # → app/build/outputs/apk/debug/app-debug.apk (~14 MB)Requires JDK 17 and the Android SDK (compile/target 35, build-tools 35, minSdk 26). The Gradle 8.13 wrapper is self-contained.
Both apps auto-select a fully scripted SimulatedTransport on a simulator/emulator: fake peers drift in and out, crowd chatter posts to channels, nearby people are addable and DM-able, and a real Ed25519-signed organizer announcement arrives ~10s after you join the demo event — verified through the genuine signature path, not faked. So you can demo the entire product, onboarding through a themed event with a verified banner, with no Bluetooth at all. iOS selects it under #if targetEnvironment(simulator); Android via SimulatedTransport.shouldSimulate(). Physical builds always use real BLE.
Interop gotcha: iOS debug builds use a testnet BLE service UUID and cannot see release builds (or Android) on the mesh. Use release builds for any real cross-device interop test.
iOS and Android are deliberate structural mirrors that meet at one contract: the 0xE0 envelope and the canonical-CBOR wire format. Both platforms reproduce spec/vectors/pack-v0.json byte-for-byte — the same payload bytes, channel addresses, packHash, and 4-emoji fingerprint — and the same vectors are run by swift test (iOS) and ./gradlew :core:test (Android). The vector pack's fingerprint is 🛸🐙💜🍀; if you change anything on the wire and the vectors don't match on both sides, it's a bug. This byte-exact discipline is the heart of keeping two native apps on one mesh.
No servers means no honeypot: messages never touch disk (in-memory only, capped, auto-purging) and there is no account, profile, or backend to breach. Your display name is the only thing shared, and only over Bluetooth. Pairwise DMs are Noise XX end-to-end encrypted; friends are pinned by their stable Noise-key fingerprint.
We are honest about v0 limitations — please read SECURITY.md before relying on Encore for anything sensitive:
- Group chat is a shared 32-byte ChaCha20-Poly1305 key distributed out-of-band. That means no forward secrecy, no member revocation, and any member can read all group traffic — past and future. It's built for "our crew's thread for the night," not long-lived secrets.
- Announcements are root-key-signed only. Staff certificates are specified but not implemented in v0.
- Trusting a pack = trusting the poster (like joining a venue's Wi-Fi). The emoji fingerprint mitigates fake posters socially, not cryptographically.
- No media — text, emoji, and cards only.
Works today:
- Native iOS (SwiftUI) and Android (Compose) apps, both building green
- Vendored bitchat BLE-mesh + Noise transport behind a narrow
ChatTransportseam - Cross-platform
0xE0wire format, byte-identical against shared vectors - Signed event packs: QR + deep link + share-over-mesh, theming takeover, offline schedule
- Verified organizer announcements (Ed25519 root key)
- Group chats (QR + 6-letter code), friends, 1:1 Noise DMs with honest delivery
- Full demo mode on simulator/emulator (real signed announcement included)
Next:
- packtool — a polished organizer web app to build, sign, and preview packs (in progress)
- Staff certificates — delegate signing so staff can post without the root key
- Hardened group crypto — sender-keys / MLS-style upgrades for forward secrecy and revocation
- Richer content beyond text/emoji/cards
Contributions are genuinely welcome — Encore is early and there's a lot of high-leverage work. Start with CONTRIBUTING.md, and look for good first issues on the tracker. The one hard rule: anything you change on the wire must keep iOS and Android byte-for-byte identical against spec/vectors/pack-v0.json — run swift test and ./gradlew :core:test and make sure both still reproduce the golden vectors. The two platforms are structural mirrors, so a change on one usually has an obvious counterpart on the other. Honesty is also a design value here: keep delivery states truthful and v0 limitations stated plainly.
| Concern | iOS | Android |
|---|---|---|
| Language / UI | Swift, SwiftUI | Kotlin, Jetpack Compose (Material3) |
| State seam | AppModel + ChatTransport protocol |
EncoreViewModel + ChatTransport interface |
| Vendored mesh | EncoreMesh (bitchat, Unlicense) |
:mesh (bitchat-android, GPL-3.0) |
| Domain layer | EncoreKit |
:core / encore-core (pure JVM) |
| Crypto | CryptoKit (Curve25519, Ed25519, ChaCha20-Poly1305) | BouncyCastle + vendored noise-java |
| Wire format | Canonical CBOR, base45 (RFC 9285), 0xE0 envelope |
same |
| QR | offline | journeyapps zxing-embedded (no Play Services) |
| Build | SwiftPM 5.9 + XcodeGen + xcodebuild | Gradle 8.13, AGP 8.10.1, Kotlin 2.2.0 |
| Min OS | iOS 16 | Android 8.0 (API 26) |
Encore stands entirely on the shoulders of bitchat and bitchat-android by permissionlesstech — the BLE-mesh + Noise transport is vendored verbatim from pinned upstream commits, and Encore's 0xE0 extension is designed to interoperate with vanilla bitchat on the same mesh. The pairwise encryption is the Noise Protocol Framework (Noise XX, Curve25519, ChaCha20-Poly1305). Thank you.
Encore is licensed per platform, because the vendored mesh layers carry different upstream licenses:
- iOS — the vendored
EncoreMeshmesh is Unlicense (public domain) from bitchat, so the iOS build contains no GPL code. - Android — the vendored
:meshmodule is GPL-3.0 from bitchat-android, which makes the combinedchat.encoreAPK GPL-3.0. - Encore-authored code (
EncoreKit,:core, the apps,packtool,spec) is permissive (Unlicense/MIT — being finalized).
When in doubt, treat the Android binary as GPL-3.0 and the iOS binary as permissive. See per-module LICENSE files and UPSTREAM-NOTICE.md for the exact provenance and the local edits (marked // ENCORE:).





