Photon is an end-to-end encrypted, federated messenger for Android, built on the Boson Network stack.
- Overview
- Photon in the Boson Ecosystem
- Features
- Security
- Architecture
- Getting Started
- Build from Source
- Testing
- Documentation
- Related Projects
- Contributing
- License
Photon is a messaging app with no company in the middle. There is no Photon account, no phone number, and no central address book. Your identity is an Ed25519 key pair generated on your device; its public key is your globally unique user id, and the private key never leaves the phone.
Photon is federated, in much the same sense that email is federated. Anyone can run a Boson super node; you pick one as your home node and connect to it, as you would pick a mail provider. Messages flow between users on different super nodes, and your identity is not owned by the node you happen to use - it is discovered through the Boson DHT, not through DNS or a provider database. Changing home nodes does not change who you are, and it does not cost you your history: the device holds the authoritative copy of your data.
What a super node does is route ciphertext and hold it briefly for offline delivery. What it does not do is read it. See Security.
Project status: version 0.5.0, feature-complete for the core messaging experience and in active development. APIs, wire formats, and storage schemas may still change between releases.
Boson Network is a two-layer peer-to-peer platform: a Kademlia DHT (layer 1) carrying identity, discovery, and signalling, and application services (layer 2) hosted on super nodes. Photon is a client of two of those layer-2 services, plus the Director's REST API.
+---------------------------------------------------------------+
| Photon (Android) |
| Jetpack Compose UI . ViewModels . Repositories |
+---------------------------------------------------------------+
| | |
Director REST Messaging client Ion Store
(HTTPS/JSON) (MQTTS, E2E encrypted) (HTTPS, encrypted blobs)
| | |
v v v
+---------------------------------------------------------------+
| Boson Super Node (layer 2) |
| Director . Messaging service . Ion Store . WebGateway |
+---------------------------------------------------------------+
|
v
+---------------------------------------------------------------+
| Boson DHT (layer 1, KadNode over UDP) |
| Ed25519 peer identity . peer + value discovery |
+---------------------------------------------------------------+
- Director - the super node's supervisor and REST API (
/api/v1/...). Photon uses it for account registration, identity binding, service discovery, profile and avatar storage, and the device registry. It issues a CWT (a CBOR-encoded, Ed25519-signed bearer token) for the authenticated routes. - Messaging service - an MQTTS broker that relays already-encrypted payloads between clients and queues them for offline devices.
- Ion Store - a content-addressed object store, used for attachments too large to inline in a message.
Photon does not run an embedded DHT node on the phone - that would be costly on battery and
awkward behind mobile NAT. It resolves the messaging and Ion Store coordinates from the Director's
GET /api/v1/client/node endpoint and then talks to those services directly. Running a full node
remains possible for self-hosted deployments; it is simply out of scope for the mobile client.
- 1-to-1 direct messages, end-to-end encrypted.
- Group channels with roles and moderation - create, join, transfer ownership, promote and demote members, kick and ban, and rotate the channel session key.
- Channel invitations in two forms: a named invite delivered as an in-chat card with Join / Ignore actions, and a shareable bearer link.
- Friend requests and contacts - request, accept, remark, and remove.
- Message actions - copy, forward to a contact or channel, save, delete, and retry a failed send.
- Local history - conversations and messages are stored on the device in an embedded SQLite database, so the app opens instantly and everything already received stays readable offline.
- Photos, files, and voice messages. Small payloads travel inline inside the encrypted message; larger ones are uploaded to Ion Store and referenced by content id.
- Voice messages recorded as Opus/Ogg with a press-and-hold composer (slide to cancel, swipe to lock) and inline playback.
- Every Ion Store object is encrypted with a fresh one-time key before it leaves the device; that key travels inside the end-to-end encrypted message, never to the server.
- Full-screen image viewer, and share-out to other apps.
- Three ways to get an account, all of which keep the private key on the device:
- Permissionless creation gated by a proof-of-work challenge - no third party involved.
- OAuth sign-in (Google, GitHub) where the operator requires it, used for onboarding only and never as the ongoing authenticator.
- Import an existing identity key, by QR scan from another device or by pasting it.
- Multi-device support - pair a new device by QR, approve it from an existing one, and watch the live session list. Devices can be deregistered and sessions revoked remotely.
- Multiple accounts on one phone - each Boson identity gets its own isolated profile (separate database, separate secrets), switchable from Settings. Signing out ends the session without destroying local data.
- Profile and avatar hosted by the Director, plus an optional passphrase gating sensitive actions such as adding a device or revealing the identity key.
- Material 3 UI with light, dark, and dynamic color; English and Simplified Chinese.
- Foreground service keeping the messaging connection alive, with local notifications for messages and friend requests (previews can be suppressed).
- Connection-state banner that distinguishes a recoverable reconnect from a terminal failure - an exceeded session quota, say - and offers the right next step.
- QR codes for your own id, for device pairing, and for the super node address.
No presence, no typing indicators, and no read receipts. All three leak behavioural metadata to anyone observing the relay, and Photon deliberately does not emit them.
Photon's security model is client-centric and trustless: the trust boundary sits at the device and at the cryptographic protocol, not at the infrastructure. You do not have to trust the super node you connect to - not for confidentiality, and not for integrity.
- Every message payload is encrypted on the sending device and decrypted only on the recipient
devices, using Curve25519 authenticated public-key encryption (libsodium
crypto_box: X25519 key agreement with XSalsa20-Poly1305), keyed from the participants' Boson identities. - Channel messages are encrypted to a channel session key held only by members, and that key can be rotated - so a removed member cannot read anything sent after their removal.
- Attachments are encrypted separately, with a random one-time XChaCha20-Poly1305 key
(libsodium
secretstream) generated per upload. The key is carried inside the end-to-end encrypted message body, so the store holds only ciphertext and never sees a key. - Payloads are authenticated, not merely encrypted: a modified ciphertext fails its MAC check and is rejected by the receiving client.
A super node is routing and temporary storage infrastructure. It is not a trusted party.
| A super node can | A super node cannot |
|---|---|
| Relay opaque ciphertext between clients | Read message text, attachments, or voice notes |
| Queue undelivered payloads for offline devices | Modify or forge content undetectably - tampering fails authentication at the client |
| Store attachment blobs it cannot decrypt | Recover an attachment key; keys never reach the server |
| Observe routing metadata (who connects, envelope addressing, timing, sizes) | Impersonate a user; it holds no user private key |
| Refuse service, or delete queued data | Read or export your private key, or silently register a device against your account |
Metadata is the honest exception. A relay necessarily observes who connects and when, and the addressing on the envelopes it routes. Photon narrows this where it can - no presence, no read receipts, encrypted and content-addressed blobs - but it does not claim to hide it.
- The user private key never leaves the device. Registration binds only the public key to the account. Even in the multi-device transfer flow the key is end-to-end encrypted to the receiving device before the Director relays it, as opaque bytes.
- Private keys are held in an encrypted store backed by the Android Keystore (hardware-backed where the device provides it), never in cleartext, and an optional passphrase can gate the operations that expose or extend them.
- The local message database and all cached media live in app-private storage.
- Keys are libsodium-style 64-byte Ed25519 private keys (seed followed by public key) throughout.
Transport security is a second layer beneath the end-to-end encryption, not a substitute for it.
- HTTPS to the Director and Ion Store; MQTTS to the messaging service.
- Services are authenticated by pinning the Boson node identity, not by trusting a public CA. A super node's TLS certificate carries a binding to its Ed25519 node key, and the client checks that binding against the node id you configured - so a self-signed operator certificate is verified with no certificate authority in the loop. Directors fronted by a real CA certificate are supported too.
- Service-layer authentication uses a CWT signed by your device key. There are no passwords anywhere in the system, and so no password database to breach.
Where an operator enables OAuth, it is onboarding-only KYC - a Sybil-resistance and profile-population step. Once a Boson identity is bound, the session is authenticated solely by that identity and no OAuth token is retained. An account created through the proof-of-work path never involves a third-party identity provider at all.
Disclaimer. Photon has not undergone an independent third-party security audit. Please evaluate it accordingly before relying on it for high-risk communication, and report suspected vulnerabilities privately (see Contributing).
Clean Architecture with MVVM, in a multi-module Gradle build. UI in Jetpack Compose, dependency
injection with Hilt, asynchrony with coroutines bridged onto the Boson libraries'
CompletableFuture API.
:app Application, MainActivity, navigation, DI wiring, foreground service,
notifications, session control, account switching
:core:model Pure-Kotlin domain entities, unified AppError model, display-profile policy
:core:designsystem Material 3 theme (light/dark/dynamic), tokens, shared components
:core:network Director REST client (Retrofit/OkHttp), identity pinning, token renewal
:core:security Android Keystore key management, encrypted secret store, profile isolation
:core:database Room storage for cold-start rendering and app-owned state
:core:boson-wrapper MessagingClient + IonStore construction, coroutine bridges, CBOR codecs
:core:qr QR encoding and camera scanning (CameraX + ML Kit)
:feature:onboarding Super node selection, registration (PoW / OAuth / import), identity binding
:feature:chat Conversations list, chat thread, forwarding, image viewer, attachments
:feature:contacts Contacts, friend requests, channels, invitations, moderation
:feature:settings Profile, devices, sessions, pairing, language, appearance
:baselineprofile Startup baseline profile generation (macrobenchmark)
Layering rule: feature:* modules depend on core:* modules and never on each other; :app
wires them together. Repositories own all Boson library access, so the UI layer never touches a
MessagingClient directly.
Photon needs a Boson super node to connect to. Either use one that someone already operates, or run your own from Boson.Releases - the release archives and DEB packages there bundle the Director, the messaging service, and Ion Store. The super node's user portal shows the address and node id you will need.
On first launch Photon asks for the super node address:
- Super Node URL - for example
https://node.example.com:9000. - Server ID (advanced) - the super node's Boson node id in base58. Required to trust a self-signed certificate; leave it blank if the node is fronted by a public CA certificate.
Scanning the QR code from the node's user portal fills both in automatically.
- Create a new account - Photon generates your key pair on the device and solves a one-time proof-of-work challenge (a few seconds; the exact time varies), then asks for a display name and an optional passphrase.
- Sign in to an existing account - import your identity key by scanning the QR code from a device where you are already signed in, or by pasting the key.
Where the operator has enabled OAuth, signing in with Google or GitHub is offered as an alternative onboarding route.
Share your Boson id - Settings shows it as text and as a QR code - add a contact by id or QR, and send. Create a channel from Contacts to start a group.
| Requirement | Version |
|---|---|
| JDK | 17 (Eclipse Temurin recommended) |
| Android SDK | Platform android-36, build-tools 36.x |
| Android Studio | Ladybug or later (optional, but the supported IDE) |
| Boson client artifacts | 3.0.2 in the local Maven repository (see below) |
Photon supports Android 13 (API 33) and later, compiles against API 36, and enables Java 8+ API desugaring because it dexes the JVM Boson stack (Vert.x 5, Netty, Jackson).
Photon resolves io.bosonnetwork:boson-messaging-client and
io.bosonnetwork:boson-ion-store-client from mavenLocal(). Build them from their own
repositories, in this order - each one installs artifacts the next depends on:
# 1. Parent POM and dependency BOM
git clone https://github.com/bosonnetwork/Boson.Parent.git
(cd Boson.Parent && ./mvnw clean install)
git clone https://github.com/bosonnetwork/Boson.Dependencies.git
(cd Boson.Dependencies && ./mvnw clean install)
# 2. Core (DHT, crypto, shared API)
git clone https://github.com/bosonnetwork/Boson.Core.git
(cd Boson.Core && ./mvnw clean install -DskipTests)
# 3. The two client libraries Photon links against
git clone https://github.com/bosonnetwork/Boson.Messaging.Client.git
(cd Boson.Messaging.Client && ./mvnw clean install -DskipTests)
git clone https://github.com/bosonnetwork/Boson.IonStore.Client.git
(cd Boson.IonStore.Client && ./mvnw clean install -DskipTests)Always include
clean. A bare incrementalinstallcan package whatever already sits intarget/classes- including classes built by the IDE's compiler, which may embed errors that surface only at runtime inside the app. Useclean installwhen publishing to~/.m2.
git clone https://github.com/bosonnetwork/PhotonMessenger.git
cd PhotonMessenger
./gradlew assembleDebug # debug APK -> app/build/outputs/apk/debug/
./gradlew installDebug # build and install on a connected device or emulatorThe Android SDK location comes from local.properties (sdk.dir=...) or the ANDROID_HOME
environment variable. Android Studio writes local.properties for you on first open.
./gradlew assembleReleaseRelease builds are minified and resource-shrunk with R8. Signing material is supplied out-of-band - never committed - through Gradle properties or environment variables:
| Property / env var | Meaning |
|---|---|
PHOTON_KEYSTORE_FILE |
Path to the keystore |
PHOTON_KEYSTORE_PASSWORD |
Keystore password |
PHOTON_KEY_ALIAS |
Signing key alias |
PHOTON_KEY_PASSWORD |
Signing key password |
When these are absent, assembleRelease still produces an unsigned APK, so the pipeline stays
testable.
- The Gradle build uses the configuration cache and parallel execution. If you hit a
configuration-cache error after changing build logic, re-run with
--no-configuration-cacheto see the underlying failure. - Netty's DNS resolver does not work on Android. Always create the shared
Vertxinstance throughBosonClientFactory.newVertx(), which disables it. - Boson client wire DTOs consumed by the app must be plain classes, not Java records: Jackson's record support fails on Android, and only in release builds.
./gradlew testDebugUnitTest # JVM unit tests (all modules)
./gradlew lintDebug # Android Lint
./gradlew connectedDebugAndroidTest # instrumented tests (device or emulator required)Unit tests are hermetic and use mockk and turbine. The instrumented suite exercises the real
Boson stack against a live super node, so it needs a reachable node and network access.
| Document | Contents |
|---|---|
docs/photon_messenger_design_spec.md |
The implementation blueprint: Boson API analysis, auth and identity flows, screen specs, architecture, roadmap |
docs/impl-plan.md |
Milestone task tracker |
docs/security-review.md |
Security review notes |
docs/persistence-option-a.md |
Local persistence design |
docs/m0-build-report.md |
Integration risk notes on running the JVM Boson stack on Android |
docs/m6-report.md |
Release-polish milestone report |
- Boson.Core - the Kademlia DHT node, Boson identity and cryptography, and the shared API every other module builds on.
- Boson.Messaging.Client - the federated end-to-end encrypted messaging client library Photon is built on.
- Boson.IonStore.Client - the content-addressed object store client library used for attachments.
- Boson.Releases - pre-built super node distributions for macOS, Linux, and Windows.
- Boson Network - all Boson Network repositories.
Contributions are welcome - code, documentation, translations, and bug reports alike.
- Fork the repository and create a feature branch.
- Make your change and add tests where applicable.
- Ensure
./gradlew testDebugUnitTest lintDebugpasses. - Open a pull request describing the change and how you verified it.
Reporting issues. Open a GitHub issue with the app version (Settings shows it), your Android version and device, and clear reproduction steps.
Reporting vulnerabilities. Please do not open a public issue for a security problem. Email support@bosonnetwork.io with the details and give us a chance to ship a fix first.
House conventions. ASCII-only punctuation in source, comments, logs, and configuration; new
source files carry the project MIT header; user-visible strings live in res/values with a
per-module prefix and must be mirrored in values-zh-rCN.
Please read the Code of Conduct before contributing.
Photon is licensed under the MIT License.
For questions or support, contact the Boson Network team at support@bosonnetwork.io.