The JVM harness renders whole screens now, not just parts. The screens take their state as `phase` and only touch the view model when something is tapped, so an unstarted model is enough — it opens the Keystore vault in `init`, which fails inside a runCatching on a background dispatcher under Robolectric. That is why this works rather than by design; if it starts hanging, look there first. With the screens visible at the kit's own 375x812 canvas, four things were obvious that were not from inside the code: 🔴 The large title said "fnreviewalpha" over a relay and a device id. The kit's title names the SCREEN — "Messages". Three lines about who you are, at the top of a list about other people, and it dominated because a handle is longer than a title. Identity belongs on Account, where it already was. Contact rows read "fnreviewbeta / fnreviewbeta · online" — the name twice and a status — because the handle is usually the same string as the display name. It is dropped when it matches. "No groups yet. Start one with your buddies." was a line of prose in the middle of a list, with a section label above it, for the ordinary state of a new account: two lines of furniture for zero rows. Both go when the section is empty AND loaded. The failure case still speaks, because the reason that message existed is real — an empty list rendering nothing gave no way to tell "you have none" from "it did not load". Alerts offered "Mark all read" on an empty screen, where its only possible effect was nothing, and it was the most prominent thing there. "Nothing new." sat against the heading like a subtitle of it. Proper empty state now, and the button appears only when something is unread. Alert rows drew initials of the KIND, so "contact request" became "CR" — two letters that look like a person's initials and name nobody. Address card: the QR was the largest thing on the Account screen and pushed everything below the fold, and the address wrapped mid-hostname as "…@rela / y.flamenet.io". A relay name split across a line break is the kind of thing somebody retypes wrong, so the host is its own line now. Two fields whose placeholder repeated the label above them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flamenet Messenger
An end-to-end encrypted messenger: the apps, the server they talk to, and a specification complete enough that someone else could build a compatible client from it.
This repository holds all of it, and is the source of truth for every part. A protocol and the thing that speaks it drifting apart in separate repositories is how "compatible" quietly stops being true — so the interop harness here compiles the iOS engine and runs it against the JavaScript one on every change, which only works while they sit in the same checkout.
engine/ the JavaScript engine, the specification, and the published vectors
server/ the Swift relay — protocol, mobile routes, and this project's website
Sources/ the iOS app (SwiftUI), including its own Swift engine
android/ the Kotlin engine. No app yet; see android/README.md
docs/ historical design notes
Two of those are also published on their own, for people who want one piece rather than the whole project:
| Repo | Mirrors | For |
|---|---|---|
| flamenet-e2e | engine/ |
The protocol, as a package |
| flamenet-relay | server/ |
Running your own relay |
Those are extracts, not forks. Changes are made here and mirrored out; a patch sent to one of them has to land here or it will be overwritten. They have already drifted once, badly, which is why it is written down.
The three engines
The protocol is implemented three times, and that is the point rather than an accident. Two implementations that agree may agree because one was written while reading the other; a third, written against the specification and the published vectors, is what finds the places where the spec says less than the first two quietly assumed.
They are checked against each other rather than trusted:
| Engine | Where | State |
|---|---|---|
| JavaScript | engine/src/ |
Complete — the reference implementation |
| Swift | Sources/E2E/ |
Complete, and compiled directly into the interop harness rather than reimplemented for it |
| Kotlin | android/engine/ |
Safety numbers and key transparency only — see android/README.md |
Running the tests
cd server && swift build # the relay binary the suite drives
cd ../engine && npm test # 478 assertions across five suites
cd ../android && ./gradlew :engine:test
npm test runs conformance, ratchet, client, interop and relay. The interop
suite compiles the iOS app's own Sources/E2E/*.swift and runs it against the
JavaScript engine, so it needs a Mac; the relay suite drives the compiled Swift
binary over real HTTP, so swift build has to have run first — a stale binary
in server/.build/ will happily pass tests for code you just changed.
Running the relay
cd server
cp .env.example .env # then fill in the secrets it asks for
docker compose up -d
Open http://localhost:8080. The relay serves this project's website itself, so a working install explains what it is rather than answering with a 404.
For production, see server/DEPLOY.md.
Building the iOS app
xcodegen generate
open FlamenetMessenger.xcodeproj
Requires Xcode and XcodeGen
(brew install xcodegen). The app is pinned to relay.flamenet.io; the
sign-in screen accepts another address for a relay you run yourself.
The app has never been released. SCOPING-v1.md tracks what stands between it
and a first TestFlight build.
What is published, and what is not
engine/ and server/ are mirrored to a public repository under the MIT
licence. The iOS app is not: the mirror is built from an allowlist rather than
an ignore list, so a new directory here is private by default instead of
published by accident. The mirror job is manual — a push to a public repository
is the one step in this pipeline that cannot be undone.
Where to read next
- The protocol:
engine/docs/SPEC.md, also served at/spec - What an auditor would need:
engine/docs/AUDIT-SCOPE.md, served at/audit - What the relay stores: served at
/metadata - What is not done: served at
/roadmap