A fast native client for Urbit: chat, calls, mail and a calendar, all on your own ship. Android, iOS, Linux, macOS, and Windows.
Pick your platform on the latest release page. iOS is on TestFlight, and ~nisfeb hands out invites.
| Platform | File | How to install |
|---|---|---|
| Android (most phones) | talon-X.Y.Z-arm64-v8a.apk |
Tap to install. May require enabling "Install unknown apps" for your browser/file manager. Android 8+ (API 26). |
| Android (older 32-bit ARM) | talon-X.Y.Z-armeabi-v7a.apk |
Same; pick this only if your device reports armeabi-v7a and not arm64-v8a. |
| Android (any architecture) | talon-X.Y.Z-universal.apk |
Larger fallback that works on every supported ABI. Use if you're not sure which split to grab. |
| macOS | Talon-X.Y.Z.dmg |
Open the DMG, drag Talon to Applications. First launch: right-click the app → Open → Open. (Unsigned, so a normal double-click is blocked by Gatekeeper.) |
| Windows | Talon-X.Y.Z.msi |
Double-click. SmartScreen may warn. Click "More info" → "Run anyway". |
| Linux (any) | Talon-x86_64.AppImage |
chmod +x Talon-x86_64.AppImage && ./Talon-x86_64.AppImage. Needs FUSE 2 (default on most desktops). |
| Debian / Ubuntu | talon_X.Y.Z-1_amd64.deb |
sudo apt install ./talon_X.Y.Z-1_amd64.deb |
Everything is self-contained. Desktop builds bundle a JRE, so you don't need Java installed.
- Sign in to your ship and use it for chat. Several ships at once, switched from the ship picker. A notification opens the ship it came for. Sign out of a ship, or delete its data, from the switcher.
- DMs (single and group), %chat group channels, threads, reactions, edits, deletes, image and file attachments, link previews, location and calendar tags, polls, voice messages. Swipe a message to quote it or open its thread. Tap an image for the fullscreen viewer with pinch to zoom, and save it to Photos or Downloads. A channel post you send carries a small clock or "!" until the host echoes it back, so a silently failing poke is visible.
- Voice and video calls between ships, and party lines for groups, with recording and transcripts. See docs/trunkline.md.
- Mail on your ship, through Grubbery's Auspex: inbox, threads drawn as a tree, labels, drafts, rules, lists, attachments and notifications, with an inbox that stays on the device between launches. A calendar with month, week and day views, several calendars, tasks, sharing, and calendars synced from ICS, CalDAV or Google. Your ship's public keys on your profile, where you can sign a message or a file and check a signature. See docs/grubbery-apps.md.
- A home screen of widgets you arrange: the sky clock (a 24-hour dial with the real sky and weather), New, chat, mail, today, statuses and the assistant. On Android the sky clock is a home-screen widget as well. See docs/home-screen.md.
- Invites by QR code, an invite-me code on your profile, and
/invitefrom the composer. Comets go by word names, never by @p. See docs/invites-and-names.md. - An assistant over your own chat history that can mail, schedule, message and call for you, by text or by voice. Loops run a saved prompt on a schedule. Catch-up summaries and emoji-react suggestions. All through Armillary on your own ship: top up a few dollars and they turn on. See docs/assistant.md.
- Semantic search across chat history. Android uses MediaPipe. Desktop uses an on-device sentence-transformer (DJL ONNX, model cached on first use under
~/.djl.ai/cache, about 30 MB). - Native OS notifications (libnotify on Linux, Notification Center on macOS, system toasts on Windows, Android and iOS), a system tray on desktop, dark, light or system theme, per-chat mute, folders. Desktop updates itself, and Android takes the APK for its ABI. An Apps page in Settings shows what the ship has and installs what is missing.
For pushes that arrive when Talon's process has been killed by Android, force-stopped, or evicted by the dataSync background-cap. Without this, notifications still work while Talon is running. They just stop the moment Android decides to reclaim the foreground service. This setup plugs that gap.
The architecture is server-relay-plus-distributor: your ship's chat events flow through a relay holding a 24×7 SSE connection, which POSTs to a per-device endpoint minted by a local push distributor app (UnifiedPush). No FCM, no Play Services.
-
Install ntfy from F-Droid (or any other UnifiedPush distributor, such as NextPush or Conversations). The Play Store build of ntfy strips the UnifiedPush distributor; F-Droid's doesn't.
F-Droid → search "ntfy" → install.
-
Open ntfy once, tap the ⋮ menu → Settings → scroll to the UnifiedPush section → confirm the toggle is on. Default server is
ntfy.sh; leave that for now unless you self-host an ntfy server. -
In Talon: Settings → Notifications → Push relay section.
- Endpoint:
https://relay.nisfeb.com(the relay I run; see below to self-host instead) - Tap Save endpoint, then Register this device → paste your ship's
+code→ Register - Status should jump to
Registered (deviceId=…)within a second - If it doesn't, tap Diagnose distributor for an inline report of what's broken (PackageManager / connector / cached endpoint)
- Endpoint:
That's it. Test by killing Talon (swipe from recents) and having someone DM you. The push should arrive on the lock screen.
The relay needs your ship's +code to log in once and derive a session cookie. The +code is encrypted at rest (AES-GCM, key derived from a relay-side master secret via PBKDF2). Same trust model as any hosted ship. If you're not comfortable with this, self-host the relay below.
The relay is a Kotlin/JVM service in relay/. Anything that can run a Docker container behind HTTPS will do. A $5 VPS is plenty.
You'll need:
- Docker
- A domain pointing at the host (
Arecord, e.g.relay.example.com) - A reverse proxy fronting it for TLS (nginx + Let's Encrypt below; Caddy works equally well)
git clone https://github.com/nisfeb/talon.git
cd talon
docker build -t talon-relay -f relay/Dockerfile .The build runs from the repo root because the Gradle wrapper + version catalog live above relay/.
This is the AES-GCM key derivation source. Treat it like a password manager root key. Back it up offline. Rotating it invalidates every ship +code already stored, so every user will have to re-register.
sudo mkdir -p /var/lib/talon-relay/data
openssl rand -hex 32 | sudo tee /var/lib/talon-relay/master-secret > /dev/null
sudo chmod 600 /var/lib/talon-relay/master-secret
sudo chown -R 1000:1000 /var/lib/talon-relay/data # uid 1000 = the in-container relay user# /etc/systemd/system/talon-relay.service
[Unit]
Description=Talon notification relay
After=docker.service network-online.target
Requires=docker.service
Wants=network-online.target
[Service]
Type=simple
Restart=always
RestartSec=5
ExecStartPre=-/usr/bin/docker rm -f talon-relay
ExecStart=/bin/bash -c "/usr/bin/docker run --rm --name talon-relay \
-p 127.0.0.1:8090:8080 \
-v /var/lib/talon-relay/data:/data \
-e RELAY_MASTER_SECRET=$(cat /var/lib/talon-relay/master-secret) \
-e RELAY_DB=/data/relay.db \
talon-relay"
ExecStop=/usr/bin/docker stop talon-relay
[Install]
WantedBy=multi-user.targetBind to 127.0.0.1:8090 (or any unused localhost port). nginx will front it. Then:
sudo systemctl daemon-reload
sudo systemctl enable --now talon-relay
sudo systemctl status talon-relay# /etc/nginx/sites-available/talon-relay
server {
listen 80;
server_name relay.example.com;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
# SSE-friendly: ship-side connections idle for minutes between events
proxy_buffering off;
proxy_read_timeout 24h;
proxy_send_timeout 24h;
}
}sudo ln -s /etc/nginx/sites-available/talon-relay /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d relay.example.com --redirect # mints + auto-renewscurl https://relay.example.com/
# {"relay":"talon","status":"ok"}In Talon: Settings → Notifications → Push relay → set Endpoint to https://relay.example.com → Save → Register → paste +code.
sudo journalctl -u talon-relay -f shows live registration / push activity. sudo journalctl -u talon-relay | grep "push whom" is the "how often is this thing actually pushing" view.
File an issue at https://github.com/nisfeb/talon/issues. Please include your platform + version, what you were doing, and what happened. Screenshots help a lot.
You only need this section if you're contributing or building Talon yourself.
- JDK 25 (Temurin, or any JDK 25 whose jlink can build a runtime)
- Android SDK 34, build-tools 34 (Android target only)
- Gradle wrapper handles its own version
# Android debug install on a connected device
./gradlew :composeApp:installDebug
# Android signed release APKs: per-ABI splits plus a
# universal fallback under composeApp/build/outputs/apk/release/
./gradlew :composeApp:assembleRelease
# Desktop self-contained app dir (host OS only)
./gradlew :composeApp:createReleaseDistributable
# Desktop installers. The package* tasks build for the host OS only.
./gradlew :composeApp:packageReleaseDeb # .deb on Linux
./gradlew :composeApp:packageReleaseDmg # .dmg on macOS
./gradlew :composeApp:packageReleaseMsi # .msi on Windows
scripts/build-appimage.sh # AppImage on LinuxThe desktop release build runs slimReleaseDistributable automatically to strip non-host native libs and unused Material Icons Extended classes. See composeApp/build.gradle.kts and CLAUDE.md for details.
composeApp/ KMP module (Android, iOS and Linux/macOS/Windows desktop)
src/commonMain/ shared screens, repos, data layer (~26k lines)
src/androidMain/ Android-only impls + background services
src/desktopMain/ Desktop impls + Main.kt entry
src/iosMain/ iOS impls behind the shared App.kt shell
src/commonTest/ pure-logic tests (run on every target)
src/desktopTest/ JVM-only tests (where most coverage lives)
.github/workflows/ release CI (per-ABI APKs + .deb/.dmg/.msi/.AppImage)
scripts/build-appimage.sh AppImage packaging
CLAUDE.md architecture + cross-platform discipline
RELEASE.md keystore + tagging procedure
Tag vX.Y.Z (matching versionName in composeApp/build.gradle.kts) and push. CI builds all platform artifacts and publishes a GitHub Release. Configure the four RELEASE_KEYSTORE_* repo secrets to enable APK signing. Without them, desktop artifacts still ship.
After cloning, install the in-repo hooks once:
./scripts/install-hooks.shThat sets core.hooksPath to scripts/hooks/, where the versioned pre-commit lives. The hook scans staged changes for personal info and secret patterns (private keys, AWS / Anthropic / OpenAI / GitHub tokens, personal emails, machine-specific paths, and so on). See scripts/hooks/pre-commit for the ruleset. Bypass for one commit with git commit --no-verify when you need to (e.g. adding a test fixture that intentionally contains a real patp).