This file handed Apple flamenetreview1 and flamenetreview2. Neither is an account on the relay, and the users table says the only review accounts are fnreviewalpha and fnreviewbeta, which are also the pair verified end to end against the live accounts service on 2026-09-04. A reviewer typing a handle that does not exist cannot get past the sign-in screen, which is the same failure that had 0.4.0 rejected under Guideline 2.1. The walkthrough was wrong in a second way. It sent the reviewer to a Community tab to add the other account. There is no Community tab, and has not been: the tabs are Home, Messages, Calls, Stories, Files, Wallet and Account, and adding somebody happens from the field at the top of Home. The two accounts are already contacts, so the step is not needed at all and the walkthrough is four steps now rather than five. Two checklist lines added. One for the Sign-In Information field, which is the one thing a reviewer cannot work around. One for re-running generate.sh after a version bump, because the version lives in project.yml and the Xcode project is generated from it, so a bump alone archives the old number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
TestFlight submission, every field, with paste-ready copy
Why you can't find "the description": TestFlight doesn't have one. The App Store Description field lives under the Distribution / App Store tab and is only needed for a public release. TestFlight's equivalent is Beta App Description, under TestFlight → Test Information.
Only external testing needs Beta App Review. Internal testers (up to 100 App Store Connect users on your team) get builds with no review and no copy at all, worth using for the first pass.
1. TestFlight → Test Information
Beta App Description
Flamenet Messenger is an end-to-end encrypted messenger you can host yourself. Sign up with a username, no phone number and no email required. One-to-one chats and group chats are encrypted on your device using the X3DH and Double Ratchet protocols, so the relay carries ciphertext it cannot read. Photos, voice notes and stories are encrypted the same way, with a per-file key.
Every conversation has a safety number you can compare with the other person to confirm nobody is in the middle. If someone's keys change, a reinstall, a new device, or an interception, the app tells you and holds the message until you decide.
This is an early beta. It is not audited, and it is built by one person in the open.
Feedback Email
<a mailbox you actually read>
Privacy Policy URL
https://flamenetmessenger.com/privacy
Marketing URL (optional)
https://flamenetmessenger.com
2. TestFlight → Beta App Review Information
Demo account
Username: fnreviewalpha
Password: <in the password store, NOT in this repository>
Second account (named in the Notes below):
Username: fnreviewbeta
Password: <the same passphrase>
The passphrase is deliberately not written here. This repository is
public and these accounts are live; committing the value hands them to anyone.
The tests read it from FN_DEMO_PASSPHRASE.
These two are the pair that exists. This file used to say flamenetreview1 and
flamenetreview2, which are not accounts on the relay and have not been for
some time: a reviewer typing those cannot get past the sign-in screen, which is
the same failure that had 0.4.0 rejected under Guideline 2.1. Checked against
the users table on 2026-09-15, fnreviewalpha and fnreviewbeta are the only
review accounts there. fnreviewalpha / fnreviewbeta were also the pair
verified end to end against the live accounts service on 2026-09-04, recorded
on ios#12.
Whatever is in the Sign-In Information field in App Store Connect has to match these two. That field is the one thing a reviewer cannot work around.
The previous pair was burned and these replace them. The old passphrase
was committed and later removed from the tree, but removing a secret from the
tree does not remove it from history, and it remained readable in three commits
on origin/main of this public repository. Because an fnid is derived from
the passphrase, that value was not a password anyone could reset: it was those
accounts, on every relay, permanently.
So the accounts were replaced rather than rotated. Rotating changes the passphrase and therefore the fnid, which leaves the pair unable to recognise each other and showing "identity changed", the state that has already made a contact permanently unmessageable on Android once.
The exposure is closed. The burned passphrase opened exactly two
accounts, appreview1demo and appreview2demo, and both were deleted
through the app's own DELETE /messenger/account route, so the
device rows, prekeys, envelopes, group memberships and buddy edges went with
them. Signing in with the old value now returns 401.
It was two, not six. The fndemo* and fnreview* pairs use a different
passphrase and were never reachable with the leaked one, worth recording,
because the first count of the blast radius was wrong in the alarming
direction, and an overstated finding costs credibility on the next one.
Deleted through the route rather than by hand in SQL. The route walks a graph, devices before prekeys so a crash cannot leave a device advertising prekeys that no longer exist, then envelopes both ways, groups owned and joined, and the buddy edges pointing back at the account. A DELETE by hand gets some of that and looks finished.
Still cruft, though not exposed: fndemo1, fndemo2, fnreview1demo,
fnreview2demo are three superseded generations of review account. Worth
tidying, not urgent.
How this pair was verified. Registration and sign-in
round-tripped against the live account service with an independently
implemented KDF (byte-compared against the app's); the app's own client stack
signed in via a unit-test probe with the passphrase supplied as bytes; and both
accounts reached the signed-in shell through the app's real screens in
consecutive UI runs. DemoAccountSetupUITests re-drives the screens whenever:
Handles burned along the way, all with unknown or stale secrets, none
holding anything: flamenetdemo1/2 (created against the relay instead of the
account service, the mistake the test header warns about, made a third time),
flamenetrv1/2/3 (UI-provisioned while simulator typing was dropping
characters). They are cruft on accounts.flamenet.io, not exposure.
xcodebuild test -project FlamenetMessenger.xcodeproj -scheme FlamenetMessenger \
-destination 'platform=iOS Simulator,id=<udid>' \
-only-testing:FlamenetMessengerUITests/DemoAccountSetupUITests
Never "verify" these with curl against relay.flamenet.io. That host is
pinned, so the app does not use the relay's own sign-in, it authenticates
against the shared FlameNet account service (accounts.flamenet.io) with a
handle. An account made on the relay answers 200 to curl and "handle not
found" to a person holding a phone.
Dead ends, for the record: appreview1demo / appreview2demo exist only on
the relay. fnreviewalpha / fnreviewbeta were made through the shared-account
path and carry no password at all.
Notes ← the field that decides whether this passes
Please read, the first account will look empty, and that is by design.
This is an end-to-end encrypted messenger. Messages are encrypted to a specific device, not to an account. When you sign in on your device you create a new device that did not exist when any earlier message was sent, so it cannot decrypt them. We cannot pre-populate a conversation for you; if we could, the encryption would not be working.
The order matters, please start with the second account.
- Sign in as fnreviewbeta (password above). Wait for the Home tab to load, then sign out from the Account tab. This registers this device for that account so it can receive encrypted messages; without it there is no key to encrypt to and the send in step 3 will fail.
- Sign in as fnreviewalpha. The two accounts are already contacts, so fnreviewbeta is on the Home tab and in Messages with no adding to do.
- Open the Messages tab, tap the conversation with fnreviewbeta, and send a message.
- Sign out, then sign in again as fnreviewbeta. The message is there.
To add somebody by hand instead, the field at the top of the Home tab says "Search or add by username". There is no separate contacts screen.
You may see a "Security keys changed" banner. That is the app working, it appears when a contact signs in on a new device, which is exactly what step 4 does. Accept it to continue.
Voice and video calls need two live devices; with one test device the call screen will open and wait. Pushes do arrive on a locked phone, so a call rings on the lock screen, but only against a relay set to the matching APNs environment. See
FN_APNS_ENVbelow.
2b. Guideline 1.2, user-generated content
Paste this into App Review Notes on the App Store submission. It answers the four things 1.2 asks for, and three of them are features. The fourth is a position, and leaving it unsaid is what makes it look like an oversight.
Reporting. Press and hold a message and choose Report; open a story and Report is beside the reactions; a profile can be reported from the conversation menu. Six reasons, a free-text note, and the person reporting chooses whether to attach a copy of what they are reporting. The report reaches the operator of the relay the account is on, by email and in a table they can read again later. The person reported is not told.
Blocking. Enforced by the relay rather than hidden in the app, so a blocked account cannot reach the person who blocked them by reinstalling. Blocking works whether or not a report is also sent.
Published contact. flamenetmessenger.com/safety says how to report without an account and names a mailbox read by people. flamenetmessenger.com /support and /contact carry the same addresses.
Filtering. This is where we differ from an app that can scan, and we would rather state it than have it look like something we forgot. The relay holds ciphertext. It has no key to any message, attachment or story, so it cannot inspect content and no server-side filter is possible without breaking the encryption the app exists to provide. What replaces it: a report carries whatever the reporting user chose to attach, which is the only way the operator sees anything; strangers cannot reach anybody silently, the first message from an unknown account raises a keep-or-block prompt; there is no discovery of strangers by browsing, only by exact handle; and an account that draws a pattern of reports can be suspended on the relay, which stops it signing in, sending or reading anything.
⚠️ One number is not settled and it is the owner's to set. 1.2 asks for "timely responses". flamenetmessenger.com/safety currently says plainly that reports are read and that no reply is sent, which is honest and is not a timeframe. Decide what the operator will actually meet before quoting one here, and put the same words on the safety page. A stated time that is missed is worse than the current silence.
3. Per build, "What to Test"
First external build. Worth exercising:
- Sign up with a username (no phone number, no email)
- One-to-one messages, replies, reactions, unsend
- Group chats, create one, add someone, send
- Photos, voice notes and stories
- The safety number on a contact's profile
- Dark mode, and text size at accessibility settings
Known gaps: no push notifications (local only, and only while the app is open), and calls need two real devices.
4. Export compliance, answered per build
- Does your app use encryption?, Yes
- Does it qualify for an exemption?, read the options and pick the publicly
available source code one if it is offered. This changed with
EXPORT-COMPLIANCE.mdv0.2: the app now goes under EAR 740.13(e), and all four repositories are public, so that answer is the accurate one. The No that used to be here was right under the mass-market route and is wrong now. - Proprietary or non-standard algorithms?, No
Paste the crypto description from EXPORT-COMPLIANCE.md when asked.
Do not set ITSAppUsesNonExemptEncryption in the plist on its own, the
upload is rejected with "Invalid Export Compliance Code". Answer the questions in
App Store Connect for the first build; ASC then issues a code for project.yml.
No recurring report. The annual BIS self-classification report belongs to the mass-market route this project no longer takes, and the 740.13(e) notification was sent once, on 2026-09-15.
5. Before you hit submit
- Both demo accounts verified signing in through the app UI.
- Walk the four steps above once on a real iPhone. Sign-in is proven; the full send-and-read round trip on one physical device is not.
sudo xcode-select -s /Applications/Xcode.app, you are pointed at Xcode 27.0 beta in iCloud Drive, and ASC rejects beta-Xcode builds.- Mint an Apple Distribution certificate for team M5923Y84P5. The only identity in the keychain is an Apple Development cert for the free personal team 2W7DA3A8W2: wrong type and wrong team.
- No provisioning profiles are installed.
- Privacy nutrition label under App Privacy, must agree with
Resources/PrivacyInfo.xcprivacy. - Age rating set.
- Sign-In Information in App Store Connect says
fnreviewalphaand the passphrase those accounts actually take. A handle that does not exist is what had 0.4.0 rejected. CURRENT_PROJECT_VERSIONbumped since the last upload, andScripts/generate.shre-run afterwards. The version lives inproject.ymland the Xcode project is generated from it, so a bump without a regenerate archives the old number and Xcode goes on showing it.FN_APNS_ENVon the relay matches where the build is going.productionfor TestFlight and the App Store,sandboxfor a build installed from Xcode. It is per-relay, not per-token, so one relay cannot serve both at once, and the wrong one fails asBadDeviceToken, which from the outside looks like calls not ringing rather than like a configuration mistake. It isproductionon the box today.
What NOT to write anywhere in this submission
- "more secure than Signal" / "better than iMessage", not supportable
- "we can't read your messages", false for the web client, and disprovable from page source. True of this iOS app; the sentence is not.
- "audited", "military-grade", "unbreakable"
- any post-quantum claim. PQ needs iOS 26+ and this app deploys to iOS 16
- Safe: "end-to-end encrypted", "X3DH and Double Ratchet", "no phone number required", "self-hosted", "open published spec", "encrypted by default, unlike Telegram"