Android client for hanami — a thin shell, not a port: a native WebView locked to your hanami server's origin, plus a NIP-55 signing bridge. hanami is server-rendered hypermedia, so the web UI is the app; every feature ships from the server and this client never diverges from it. No server code lives here.
- Server screen (native): enter your instance's address — a public URL, a LAN IP, or a Tailscale IP. The WebView and the signing bridge are both bound to that origin.
- WebView: loads hanami's own UI. Off-origin navigation opens in the system browser and never sees the signing bridge (the origin lock is a security invariant).
- NIP-55 bridge: a
window.nostrshim injected at document-start routessignEvent/nip44.encrypt/nip44.decryptto your NIP-55 signer app (Signet, Amber, any compliant signer). To the server it's indistinguishable from a desktop NIP-07 extension, so hanami's existing browser-signing flow works unchanged.
- Sign in with any NIP-55 signer — one upfront approval, then silent signing via the signer's ContentResolver (per-request intent fallback if the signer declines silent mode)
- Native NIP-98 session handshake: the server's sign-in page never renders
- File uploads (system chooser) and downloads (DownloadManager, session-authenticated)
- Sign-out / change-server flow with a native sign-in-error surface
- Edge-to-edge UI, light/dark themes
- Release builds are R8-minified and v2-signed; the APK is ~2 MB
- Android 8.0+ (
minSdk 26) - A hanami server you can reach
- A NIP-55 signer app installed (e.g. Signet or Amber)
Grab the APK from Releases. Zapstore publishing is coming next.
- Works over plain
http://on LAN/Tailscale IPs (cleartext is permitted via the network security config; hanami's session cookie is onlySecureon https, so sessions work). - A Tailscale MagicDNS hostname needs an
allowed_hostsentry in the server'sconfig.toml(IP literals always pass hanami's host guard). - No hanami changes required; the shell appends
hanami-android/<ver>to the UA if the server ever wants to tailor copy.
- Login:
nostrsigner:intent withtype=get_public_keyand no package — Android's chooser picks the signer; the result carries the npub and the signer's package name. - Background permissions requested upfront (one approval, then silent signing):
sign_eventfor kinds 27235 (sign-in), 24242 (Blossom auth), 30630 (drive nodes), 30078 (app-data), 5 (deletes), 10063/10002 (server/relay lists), plusnip44_encryptandnip44_decrypt. - After login: silent signing via the signer's ContentResolver
(
content://<package>.SIGN_EVENT, args[payload, pubkey, current_user]). A null cursor falls back to the intent; arejectedcolumn does NOT (the user said no).
Open in Android Studio (SDK 35). Kotlin, classic Views; dependencies are just appcompat/material/androidx.webkit — deliberately thin, like the server it fronts.
./gradlew assembleRelease produces a signed, minified APK when keystore.properties
(gitignored) and the keystore are present. A fresh clone without them still builds — the
release just falls back to unsigned; only the maintainer's checkout produces the
installable, stably-signed artifact.
- Share-target upload and camera-roll backup
- Bunker (NIP-46) sign-in — needs zero native work, the server holds that signer
- Multi-server switching
- FIPS URLs (needs an on-device transport)