Your home's energy, on the phone.
Native iOS (SwiftUI) and Android (Jetpack Compose). Shared logic is Kotlin Multiplatform: pairing, passkeys, Noise, the relay, the session. The box at home is the record. This app is a cached projection of it. The cloud is blind.
Not a wrap of the web app. Same protocol,
same QR, same relay, same RP ID (app.ftw.energy).
Follow FTW's shared vision and roadmap. The target experience needs few routine choices, honest live feedback and reliable daily charging. The native status below is a dated record and its verification gates still apply; the vision does not declare new native features shipped.
Sourceful develops this app. PRs are welcome, preferably based on issues. Share a short Markdown proposal or a focused fix with relevant test evidence. See CONTRIBUTING.md.
SwiftUI / Compose
│
▼
shared (KMP) — enrollment, vault, Noise IK, frames, session, relay
│
▼
wss://relay.ftw.energy (encrypted)
│
▼
FTW box
Two taps: scan the QR on the box, Face ID / biometrics, the house.
V1 is Pair + Now. That is on main as of 2026-08-22. Not a wrap of the web
app. Not Flutter, not React Native.
In the apps today
- Scan or paste a v2 pairing QR (
https://app.ftw.energy/p#v2.…). - One passkey prompt at enroll. RP ID
app.ftw.energy. PRF saltftw.prf.v1.vault. - Noise_IK_25519_ChaChaPoly_SHA256 to the box through
wss://relay.ftw.energy. - Now shows headline plus grid / solar / battery / house from frozen field ids.
- Vault, site and last readings live in iOS Keychain / Android EncryptedSharedPreferences. Cold start paints from cache, then reconnects without Face ID. Forget wipes the store.
Proven here
| Check | Result |
|---|---|
./gradlew :shared:jvmTest |
Green |
Live box e2e (127.0.0.1:18080 + production relay) |
hello_ok + snapshot, phase streaming |
| iOS Simulator (iPhone 17, iOS 26.5) | Built and launched |
Android emulator FTW_Phone (API 35 ARM64) |
APK installed, Pair shown twice |
Passkey PRF cannot run on the JVM. Live e2e uses a local wrapping key for the ceremony and the real Noise / relay / box path. The Android emulator has no camera feed — paste the pairing link.
Not v1 (do not start these next)
Energy, History, Plan, EV, commands, escrow restore, spoken codes, LAN, WebRTC, push, App Store / Play listing.
Known holes
srcStateshould follow the Now fields'srcIdin the dict, not every driver on the site.- Wrap key is raw PRF bytes, not the web app's HKDF. A native vault will not open in the PWA, and the other way around.
PasskeyHost.enrollfrom Kotlin still blocks. The UIs call the async ceremony and skip that path.- Field ids in
Explanation.ktare still hand-written; they should come fromprotocol/registry.yaml.
JDK 21.
export JAVA_HOME="$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home"
./gradlew :shared:jvmTestThat suite covers enrollment URLs, Cacophony Noise IK, hello/sub CBOR matching the box's interop hex, 512-byte lane 0 frames, the vault, and a session that turns hello_ok + snap into Now state.
Start a box (make dev in srcfl/ftw, or
go run ./cmd/ftw with app_link.enabled: true). The box joins
wss://relay.ftw.energy — that origin is fixed on the box, so the client
meets it there.
export FTW_LIVE_BOX=127.0.0.1:8080 # or 18080 if the API port moved
export FTW_LIVE_RELAY=wss://relay.ftw.energy
./scripts/e2e-ftw.shThe test mints POST /api/app-link/pairing with {"role":"owner"}, runs the
shipped connectToSite path (Noise IK, pairing code in handshake message 1,
prologue ftw.session.v1: + box static), and asserts hello_ok plus a
snapshot that includes the frozen field ids.
iOS: open iosApp/iosApp.xcodeproj. SwiftUI Pair (camera QR + paste) and Now.
Xcode 16+, iOS 18 for passkey PRF. A Run Script build phase compiles the
Shared framework with
./gradlew :shared:embedAndSignAppleFrameworkForXcode.
Android: ./gradlew :androidApp:assembleDebug (minSdk 28). Pair uses CameraX
- ML Kit for the QR. Passkeys go through Credential Manager.
Emulator (AVD FTW_Phone, API 35 ARM64 Google APIs):
export ANDROID_HOME="$HOME/Android/sdk"
"$ANDROID_HOME/emulator/emulator" -avd FTW_Phone
adb install -r androidApp/build/outputs/apk/debug/androidApp-debug.apk
adb shell am start -n energy.ftw.app/.MainActivityRP ID app.ftw.energy. PRF salt ftw.prf.v1.vault. Reading uses a local
wrapping copy so Now paints without a passkey prompt.
| Path | What |
|---|---|
shared/ |
KMP: identity, crypto, protocol, relay, session |
androidApp/ |
Compose UI |
iosApp/ |
SwiftUI UI |
protocol/registry.yaml |
Names shared with the box |
scripts/e2e-ftw.sh |
Live box e2e |
AGPL-3.0-only with the Energyplan combination permission. See LICENSE, LICENSING.md and NOTICE. Earlier Apache-licensed versions retain their earlier grants.