Privacy policy
Indoor Bike stores everything on your device. We don't run a server, we don't have analytics, we don't have accounts. The only time data leaves your phone is when you explicitly ask it to: uploading a ride to a third-party service you've configured, or sharing a workout file.
This page describes, in plain language, exactly what the Indoor Bike Android app and the indoorbike.app website do with data. It applies to the version of the app and site published at the date above. If anything changes materially, we'll update the date and call out the change in the project's release notes.
The Android app
What's stored on your device
- Profile values: your FTP, weight, and max heart rate, used to compute targets and zones.
- Paired-device identifiers: the Bluetooth MAC of your trainer, heart-rate monitor, and controller, so the app can reconnect on next launch without re-pairing.
- Workouts: bundled JSON shipped with the app, plus any workouts you import (file pick, share-sheet, QR, by URL, or picked from a community repo in the in-app browser). Imported workouts live as plain JSON in the app's private storage; ones added from a known community repo also carry a small sidecar file recording the source repo's name for attribution.
- Cached community registry: a read-only JSON list of community-repo names + URLs (whatever
indoorbike.app/registry.jsonreturned the last time you opened More Workouts), kept on-device so the list still shows when you're offline. - Ride history: the result of each ride (duration, average power / cadence / heart rate, plus a 1-second-resolution telemetry log) in a local SQLite database.
- Optional integration credentials: if you set them, your Intervals.icu athlete ID and API key, stored in the same on-device key/value store as your profile values. The API key is excluded from Android's Auto Backup (it never leaves your device via that channel).
- Optional sync-folder URI: if you've configured Backup & sync in Settings, the app stores the location of the folder you picked so it can read and write its slice on each sync. The folder itself can be anywhere your file picker reaches - local storage, Drive, Dropbox, Nextcloud, Syncthing, …; the choice is yours and is made explicitly via Android's system folder picker.
- Backup & sync slices (only when you've configured a sync folder): a single JSON file per device, written into the folder you chose. Each slice carries: the rides you've recorded on this device (date, duration, average power / cadence / heart rate - not the per-second telemetry), the imported workouts in your library (with the source-repo attribution sidecar), your profile values (FTP, weight, max HR, onboarding flag, favourites, Intervals.icu athlete ID and auto-upload toggle), and a stable per-install device id + label. The Intervals.icu API key is never written to a slice. Bluetooth pairings are not written either; they're hardware-local.
All of the above lives in the app's private storage on your phone - apart from the optional sync slices, which live in whichever folder you picked via Android's system folder picker. No account, no central server, no server-side copy of anything we control. Uninstalling the app removes its private storage; sync slices in your chosen folder remain there for you to delete manually if you no longer want them.
What goes over Bluetooth
The app speaks the standard Bluetooth Low Energy FTMS (Fitness Machine Service) profile to your trainer, the standard Heart Rate Service to a heart-rate monitor, and a vendor service to a Zwift Click controller where applicable. Communication is point-to-point with your devices. It doesn't go to a server.
Permissions the app requests
- Bluetooth (scan + connect): required to discover and talk to your trainer, HRM, and controller.
- Camera: only used while the in-app QR scanner is open, to read workout-import codes. Frames are processed locally and never stored.
- Notifications (Android 13+): requested on first ride so the foreground "ride in progress" notification can show.
- Location (Android 11 and earlier): the OS requires this to allow Bluetooth scans on older Android versions. We don't use location in any other way and don't store coordinates.
The app is built without Google Play Services, Firebase, Crashlytics, or any analytics SDK. The only external network connections it can make are the ones you explicitly initiate (workout / repository imports, and the optional integration described next).
Optional Intervals.icu sync
Intervals.icu is a third-party training analytics service. The integration is off by default. If you enable it in Settings → Cloud sync, the app:
- Stores the athlete ID and API key you provide on your device.
-
Uploads ride files to
intervals.icu's API when you manually tap Upload, or (if you turn on auto-upload) once per ride on completion. Each upload is a single POST containing the ride's FIT file (power, cadence, heart-rate trace). - Does not share workouts, settings, or any other app data with intervals.icu.
Once a ride is uploaded, the data is governed by Intervals.icu's own privacy policy. Disabling the integration in Settings clears the credentials and stops further uploads; rides already uploaded stay with intervals.icu unless you delete them there.
The website
indoorbike.app is a static site. There is no backend, no database, no account system, and no analytics. The site does not set first-party cookies. The bullets below describe each page's data behaviour.
Browsing the homepage and community pages
Loading any page transmits only the standard HTTP request headers your browser sends to any website (IP address, User-Agent). The static-site host's server logs may retain these for an operator-defined period to detect abuse; we don't ingest, aggregate, or analyse those logs for product purposes.
The community browser (/community/) fetches static JSON
files: /registry.json on first load, then each
repository's manifest when you click into it. These are normal HTTPS
GETs to the same site or to whichever host the repository's manifest
lives on. No data about your visit is sent to those hosts beyond what
the request itself carries.
The workout editor
The editor is fully client-side. The workout you build is saved to your
browser's localStorage under a key the editor owns
(indoorbike.editor.workout.v1) and your FTP setting under
indoorbike.editor.ftp.v1. Both stay on your device; the
editor does not transmit them anywhere. Clearing site data in your
browser removes them.
When you generate a share link or QR code, the workout is encoded into
the URL itself as a hash fragment (#wz=…). Browsers
never send the hash fragment to the server, so the receiving
page can decode the workout entirely client-side. The editor's "Send
to phone" feature uses the browser's standard share API; the file is
handed to whichever app you select. No copy goes through us.
The /share/ page
Visiting a share link with a workout in the URL means your browser decodes the workout from the hash fragment and renders it. As above, the hash fragment never leaves your browser. The "Send to Indoor Bike" button uses the browser's share API; the file is offered to your installed apps and never returns to us.
Third-party assets we load
The website pulls in a small number of static assets from public CDNs. Loading them causes your browser to make a request directly to those hosts; their privacy and logging policies apply to those connections. We don't add tracking parameters or send any custom data with the requests.
-
jsDelivr (
cdn.jsdelivr.net): two small libraries.qrcode-generator(used by the editor to draw QR codes) andpako(used by the editor, community, and share pages to gzip / un-gzip workout payloads in URLs).
Fonts are self-hosted; the landing page loads no third-party resources at all. We may self-host the two editor libraries in a future release to remove the remaining dependency.
Cookies, tracking, advertising
None. The site does not set first-party cookies. There is no advertising, no fingerprinting, no analytics, no A/B-test framework. The hosting provider may set technical cookies for protocol-level reasons (e.g. CDN routing); these are operational and not used for any product purpose by us.
Children
The app and site are not directed at children under 13. We don't knowingly collect data from anyone, of any age. See above.
Changes to this policy
Material changes will be reflected by updating the "Last updated" date at the top and called out in the app's and website's release notes. Past versions of this policy are visible in the project's git history on Codeberg.
Contact
For privacy questions or data-related requests, the fastest route is to open an issue on the project's Codeberg repository. Sensitive matters can be flagged in a private issue.