New
Apps 1,895Makers 1,890This week 1,690
Promote your app−30%
Bati screenshot 1
1views in
the last 30 days

Bati

Dark-fantasy fitness RPG built with Expo, React Native and Tamagui. Offline-first on SQLite...

Built with Claude Code

by guiforge · Unclaimed · from FRFrance · added Oct 29, 2026
no ratings0 comments

Build your body. Build your village.

Bati

Train like a hero, build like a king.

A dark-fantasy fitness RPG. Your workouts are quests, your reps are damage, and the village you build is made of what you actually lifted.


getbati.app, the site, in English, French, German, Spanish and Russian

CI Release Licence: MIT

Expo 57 React Native 0.86 TypeScript strict SQLite + Drizzle

A workout is a quest. Your reps land as damage on a boss. The muscles you actually train raise the buildings of a village that grows out of what you lifted, not out of what you bought.

Sport first, RPG second: the training logic leads and the game amplifies it. Nothing decorative gets between you and your next set.

It is built for people who train alone and lose the thread, at home, in a gym, or on a road out of town. No coach, no feed, no leaderboard, nobody to compare yourself to. The only thing keeping score is a village that can only be built by showing up.

Your training stays on your phone. No account, no servers of ours, no analytics, no ads. Your history lives in a database on the device. Three things can reach the network, each behind its own switch in Settings, all three off by default.

First, the map behind an expedition's route. Switched on, the phone asks OpenFreeMap (OpenStreetMap data, served from tiles.openfreemap.org) for tiles, the square images a map is made of, covering the place where you went. That request tells the host roughly where you were, with your IP address and the time. Not the route to the metre, not the pace, not the training, not who you are, but the area, yes. Off, the route is drawn on a plain background and nothing is requested.

Second, Check for updates: switched on, the app asks api.github.com once a day whether a newer version has been published, which is how a copy installed from an APK by hand finds out at all. That request carries no identifier, downloads nothing, and the card it raises opens the release page in your own browser.

Third, Sync my devices: switched on, each device encrypts its whole history on the phone (AES-256-GCM, with your backup key) and sends it to a WebDAV or Nextcloud server you choose, which stores bytes it cannot read. Pointed at a folder that Syncthing keeps in step instead, Bati makes no request at all.

Nothing else may open a connection: a lint rule rejects every network call written in JavaScript outside src/updateCheck.ts and src/cloudSync.ts, so a fourth destination cannot arrive by accident. The map is fetched natively by MapLibre, from the one host its style names. Three switches; everything else is the architecture.

Early days. The app works end to end and is published through F-Droid, which is where its first users came from. It is not on Google Play: that account exists and has never left its internal track.

See it

Home: your next session, one tap away Mid-session: one exercise, a rep counter and a done button Boss fight: the boss above a health bar and the rep counter Village: buildings raised by past sessions
Your next session, one tap away. One exercise at a time. Some sessions fight back. Your reps built all of this.

What it does

Every workout is a quest. A campaign with a story, chapters and a map. You pick the next quest, and it knows what you trained last.

Your reps land as damage. Finish a set and the boss takes the hit. The bar moves because you moved, and the fight is over when the session is.

Some sessions fight back. Boss fights close a chapter and ask for a real effort: the one session in the arc you have to show up for.

The village is made of what you lifted. Train shoulders, the forge rises. Nothing in it can be bought, skipped or rushed; every building is a receipt for work you actually did.

Some quests leave the walls. Walking, running and riding are expeditions: a session measured in ground covered rather than repetitions. The distance comes from the app's own GPS module, written against Android's LocationManager with no Google library anywhere near it, and the leagues it records raise the High Road, the one building in the village that nothing you lift can level.

Years of history, on one screen. Streaks, records, muscle balance and every session you ever finished, read from a database on your phone, with nothing to log in to.

No account, and three network destinations, all off. There is no server of ours to leak, no analytics to opt out of, and no cloud copy of your training unless you name the server. Bati reaches out for three things and only if you ask it to: the map behind an expedition, from tiles.openfreemap.org, which tells that host roughly where the outing happened; a daily question to api.github.com about a newer version, which tells it an IP address and a time; and device sync, to your own WebDAV or Nextcloud server, which receives your history encrypted on the phone. Nothing else leaves the phone. Privacy policy.

Install it

Bati is for people aged 16 and over.

All of this is also on the site, in English, French, German, Spanish and Russian.

F-Droid, the one that updates itself

Bati is in the official F-Droid catalogue: that is what the badge above opens, and searching F-Droid for "Bati" finds it. Installed from GitHub Releases before that? F-Droid signs its builds with its own key, so the catalogue cannot update that copy (Android refuses a different signature, and uninstalling takes your hero with it). Keep the self-hosted repository instead (same APKs, same key, updates arriving like any other app's), described in docs/fdroid.md.

Add the repository to F-Droid

By hand instead, under Settings → Repositories → +: paste the address, and check that F-Droid shows this fingerprint before you accept it:

https://getbati.app/fdroid/repo
089D B128 38D6 60CA F285 BE85 5D8E 6D02 3407 A50D 9805 1B38 4309 5EA0 9BBA 2D97

That fingerprint is the whole security model of a self-hosted repository: it pins the key every future update must be signed with. Adding the address without checking it trusts whatever answers at that URL.

A plain APK

Releases: Android will ask you to allow installs from an unknown source the first time. Nothing updates itself this way; you download the next one yourself.

See docs/fdroid.md for how the repository is built and signed.


Everything below is for reading or building the code.

Written with AI

This project is largely written with AI assistance. Saying it up front so you read the code with that in mind. It is a fun project: I build it because I enjoy building it.

Where the art comes from

Every illustration in assets/ (exercise art, quest and adventure covers, boss portraits, village buildings, avatars) is AI-generated. Nothing is stock.

All of it comes from FLUX by Black Forest Labs (FLUX.2, and FLUX 3 for the latest retouches), through our own API account. That detail is the licence: BFL's terms give the output to whoever holds the key, so generating through an aggregator would have left us with art we could not license onward, which is why an earlier Midjourney-and-Gemini-via-Mammouth set was regenerated from scratch.

Many exercise pictures are drawn against a pose reference: line art from workout-guide, a photograph (mine, or one from Wikimedia Commons), or a posed 3D mannequin. The references themselves stay out of the repo, some of them being photos of me. Their credits stay in scripts/pose-refs/, and the ledger pins each one by hash.

Icons are a separate system: game and fantasy icons go through the project's own icon hook, utility icons come from @tamagui/lucide-icons.

Quick start

npm install
npm start          # Expo dev server

Running on a device needs a dev build (npm run android / npm run ios), because the app uses native modules: SQLite, audio, an Android home-screen widget, MapLibre, and modules/bati-location, the location service this repo owns. Expo Go loads none of them.

Scripts

Command What it runs
npm start Expo dev server
npm run android / ios / web run on a target
npm run check Biome + TypeScript
npm test Jest
npm run deadcode dead-code check
npm run maestro Maestro E2E flows (needs a device)
npm run db:generate / db:push Drizzle migrations

Quality gates

All of these run in CI; the first four also run on commit or push.

  • Biome: formatting and lint, including four GritQL plugins written for this repo. One rejects raw hex colours outside constants/rawColors.ts; another (noJsNetwork.grit) rejects fetch, XMLHttpRequest, WebSocket, EventSource and sendBeacon anywhere in the app but the one module that asks GitHub about new versions, which is what keeps "two hosts, both off" a fact rather than a sentence in this file.
  • TypeScript, strict.
  • Jest: ~1200 tests, with coverage thresholds set just under actual so they catch deletion rather than reward padding.
  • Knip: dead code. At zero; anything it reports is new.
  • expo-doctor: 20 checks on dependency versions and project shape, all green. A version the installed SDK does not expect builds fine here and fails for whoever rebuilds from source.
  • Maestro: end-to-end flows against a real device.

Project layout

  • app/: Expo Router screens (file-based routing)
  • components/: UI, grouped by feature
  • db/: SQLite + Drizzle domain logic; the source of truth for game rules
  • stores/: Zustand state (session, settings, user)
  • constants/: game design constants and the colour palette
  • modules/: local Expo modules; bati-location is the Kotlin GPS service
  • docs/: the wiki: product, design, gameplay, architecture, planning
  • .maestro/: E2E flows
  • plugins/: local Expo config plugins

Contributing

CONTRIBUTING.md for code, docs/CONTRIBUTING.md for the wiki, SECURITY.md to report something that should not be a public issue. Or just write to contact@getbati.app. An idea is welcome in whatever form it arrives.

Licence

MIT for the code, CC BY-SA 4.0 for the artwork in assets/.

The illustrations are generated with FLUX by Black Forest Labs through our own API account. BFL's Developer Terms give the account holder the output and allow personal and commercial use (§3.b, EU terms §4.b). BFL also signs every output with C2PA Content Credentials. Resizing and converting the pictures to WebP for the app drops them, so the signed originals are kept, and this is the place that says so: the art is AI-generated. scripts/provenance.json records the model, prompt, seed and references behind every picture, so fork away: the art comes with you, and you can regenerate it yourself.

Many exercise illustrations are derived work on top of that. FLUX redrew them from line art by Bryl Lim in workout-guide, which builds on the anatomical studies of Everkinetic, both CC BY-SA 4.0. The share-alike obligation is already satisfied by the licence above. It binds the images, never the app code. scripts/provenance.json names the source for each one.

The tuck planche takes its pose from a photograph by Mr. Yoga (CC BY-SA 4.0), reduced to its outline before FLUX redrew it. Seven more take theirs from photographs of the maintainer, released under the same licence. The app's Credits screen carries the same attributions, since the APK does not ship this file.

Two exceptions keep their own terms: the game-icons.net set (CC BY 3.0 / CC0) and the store badges.

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#health · #rpg · #mobile