Security¶
This page covers what the app stores, where it sends the API token, and its cleartext HTTP. To report a vulnerability, follow the repository's security policy. The server's security page covers its tokens, routes and limits.
The app collects no telemetry or analytics and sends no crash report to any service. It talks only to the server you set and, for the update check, to GitHub, as the next sections describe. The one thing it reports is a crash, to your own server, as crash reports describes.
What the app stores¶
The server address and API token are saved with the other settings, in the app's private storage or, on the web, the page origin's localStorage. They are not encrypted at rest. A Settings PIN is stored beside them as a salted SHA-256 hash, never the digits, and is never sent to the server. Storage lists where on each platform. On Android, app backup and device-to-device transfer exclude them (allowBackup is off and data_extraction_rules.xml excludes all data).
- Token. Sent only in the
Authorizationheader of requests to the server's/api/v1API. The app does not log it, and Settings shows only its last four characters while the field is not being edited. - Pairing. A code sends the code, the app's client id, platform and device name to
/api/v1/pairwithout a token. The server answers with a token of this device's own, which the app saves like one typed in. sofaking://pairlinks. Any web page, QR code or app can open one. It pairs at once only on a device with no server or with the server it names (same scheme, host and port). A link naming another server asks first and changes nothing until you confirm (links).- Server-side state. With a paired or configured server, hidden channels, favorites and recent channels live on the server under the client id, where the admin can see and forget them. Forgetting a paired device also revokes its token, and the app then needs pairing again.
- No token. Media requests under
/media(including library and recording playback under/media/vod), logos under/logosand library artwork under/artworkcarry none, since image loaders fetch without one. - Update check. Settings asks GitHub's public releases API for the
snapshotrelease, with no token or client id. On Android, Install update downloads the release's APK and installs it only after its SHA-256 matches the digest GitHub records for the asset, its package name is the app's, its signing certificates are the installed app's and its version code is higher; Android's installer enforces the key and version again. The app holdsREQUEST_INSTALL_PACKAGES(which Android grants only after the viewer allows it to install apps) andUPDATE_PACKAGES_WITHOUT_USER_ACTION(an update the viewer asked for skips the second prompt).
Crash reports¶
When the Android or desktop app dies on an uncaught exception, it writes the last three crashes to its private storage (crashes/ beside the other files, see storage) before the process ends. At the next start, with a server set and Settings › Diagnostics › Send crash reports to your server on, which it is by default, the app posts each one to /api/v1/diagnostics/crashes on that server with the same token as its other requests, and deletes it once the server answers. A report holds the exception class, a message with every address, token, e-mail and quoted text removed (none at all for a network failure), the stack frames, the app version, the system version, the device model and the time. It holds no server address, token, channel, title or device name. The server scrubs it again, shows it to its admin and to an AI assistant on its MCP endpoint, keeps the newest 100 and forwards none (server side). Turning the setting off sends nothing and deletes the crashes kept on the device. A server too old to take reports leaves them on the device, and the web and Apple apps keep none.
Local network¶
The server is on the local network, which Android 17 opens to an app only with the ACCESS_LOCAL_NETWORK permission, part of Nearby devices. The Android app declares it and asks for it at its first start on Android 17; denied, it cannot reach or find the server until the permission is allowed in the system's app settings. Apple TV and iOS ask for local network access the first time the app reaches the server. Settings browses for servers over mDNS (_sofaking._tcp) only while its server fields are open.
The Android app declares FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PLAYBACK for MusicService, which runs only while a song is loaded. The service is exported, as Media3 requires, so that the system's media controls and other controllers can bind to it; they reach only the music's session, which plays, pauses and steps through the queue already playing.
Cleartext HTTP¶
The app allows cleartext HTTP (network_security_config.xml on Android, NSAllowsArbitraryLoads on iOS and Apple TV) so it can reach a Sofa King server on your LAN, which serves plain HTTP. A web app served over HTTPS cannot call a plain-HTTP server; browsers block the mixed content. Traffic to that server, including the token and the streams, is readable by anyone who can observe the network path.
If the server is reachable beyond your LAN, serve it over HTTPS and enter its https:// address in Settings. Set SOFA_API_TOKEN on the server in either case.
Verify a download¶
Every package on the releases, the APK, the .deb, the DMG, the MSI and the web archives, carries a GitHub build attestation that ties the file to the commit and the workflow run that built it. With the GitHub CLI, check one before installing it:
gh attestation verify sofa-king-snapshot.apk --repo jamesarich/sofa-king
A file that someone else built or changed fails the check. The attestation proves where a file came from, not that it is signed for its platform: the desktop packages are still unsigned, so macOS and Windows ask before the first start.