A fitness tracking app with a Go REST API backend and a native Android client built with Kotlin and Jetpack Compose.
- Program management — create and organize workout programs with custom exercise selections
- Active workout tracking — log sets, reps, and weight in real time with a built-in rest timer
- Progressive overload — per-exercise settings for working weight, target reps/sets, auto-increment amounts, and deload percentages
- Progress analytics — view exercise history, weight progression charts, and body weight trends
- Workout history — browse and delete past sessions
- Offline-first — workouts are cached locally with Room and synced to the API when available
- Theming — Material Design 3 with multiple color presets, persisted across sessions
| Layer | Technology |
|---|---|
| Backend | Go 1.22, Chi v5, pgx/v5 |
| Database | PostgreSQL 16 |
| Android | Kotlin, Jetpack Compose, Room, Retrofit |
| Infrastructure | Docker, Docker Compose, Caddy (HTTPS), DuckDNS |
Workouts/
├── api/ # Go REST API
│ ├── main.go # Server setup, routing, migrations
│ ├── handlers.go # HTTP handlers
│ ├── store.go # Database queries
│ ├── models.go # Shared data structures
│ ├── seed.go # Exercise catalog seeding
│ ├── Dockerfile
│ └── migrations/ # SQL migration files
├── android/ # Kotlin Android app
│ └── app/src/main/java/com/workouts/app/
│ ├── data/ # Room DB, Retrofit API, Repository
│ └── ui/ # Compose screens and ViewModel
└── docker-compose.yml
Prerequisites: Docker and Docker Compose
For local development, start just the API and database:
docker compose up api dbThis starts the Go API on port 9001 and PostgreSQL on port 5432. The database is initialized and seeded with a built-in exercise catalog automatically.
The full stack (docker compose up, requires a .env — see .env.example) adds two services for internet-facing deployment:
- caddy — HTTPS reverse proxy with automatic Let's Encrypt certificates, listening on host ports
8081(HTTP) and8443(HTTPS). The router must forward external80 → 8081and443 → 8443. - duckdns — keeps the DuckDNS subdomain pointed at the network's public IP.
To run without Docker:
cd api
DATABASE_URL=postgres://user:password@localhost:5432/workouts go run .Prerequisites: Android Studio, Android SDK (API 26+)
- Open the
android/directory in Android Studio. - The server URL is compiled in as
ApiService.BASE_URL— point it at your API host (e.g.http://10.0.2.2:8080for the emulator) when developing. - Build and run on a device or emulator (API 26+).
All data on the server is scoped to a user, identified by a device token. The Android app generates a random UUID on first launch, stores it in SharedPreferences, and sends it on every request in the X-Device-Token header. The API creates a user automatically the first time it sees a new token, so no sign-up is needed — each fresh install gets its own empty account. Requests without a token are rejected with 401.
To attach a device to an existing user manually (instead of via Google account linking), find its auto-created token in the device_tokens table and re-point it:
UPDATE device_tokens SET user_id = <user id> WHERE token = '<token>';Data created before migration 006_users is assigned to a single legacy user (the first row in users).
A user can link a Google account from Settings → Google Account. The app obtains an ID token via Credential Manager and posts it to /api/auth/google; the API verifies it against GOOGLE_CLIENT_ID and stores the Google identity on the user. Linking the same Google account from a new device re-points that device's token at the existing user and merges any data — this is the device-loss recovery path. No account is required to use the app.
All endpoints are prefixed with /api and require the X-Device-Token header.
| Method | Path | Description |
|---|---|---|
GET/POST/DELETE |
/api/auth/google |
Get, create, or remove the Google account link |
GET |
/api/exercises |
List exercise catalog |
GET/POST |
/api/programs |
List or create programs |
GET/PUT/DELETE |
/api/programs/{id} |
Get, rename, or delete a program |
PUT |
/api/programs/reorder |
Reorder programs |
POST/DELETE |
/api/programs/{id}/exercises |
Add or remove exercises from a program |
GET/PUT |
/api/programs/{programID}/exercises/{exerciseID}/settings |
Get or update exercise settings |
GET/POST |
/api/workouts |
List or submit a workout |
POST |
/api/workouts/import |
Batch import workouts |
GET/DELETE |
/api/workouts/{id} |
Get or delete a workout |
PATCH |
/api/workouts/{id}/bodyweight |
Update body weight for a workout |
DELETE |
/api/workouts/all |
Delete all workout history |
GET |
/api/progress/exercises |
Exercises with full history |
GET |
/api/progress/exercise/{exerciseID} |
Progress for a specific exercise |
GET |
/api/progress/bodyweight |
Body weight history |
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgres://...localhost... |
PostgreSQL connection string |
PORT |
8080 |
API listen port (compose sets 9001) |
DUCKDNS_TOKEN |
— | DuckDNS account token (compose .env, duckdns service only) |
GOOGLE_CLIENT_ID |
— | OAuth web client ID for Google account linking; linking is disabled when unset |
The Android app's server URL is a compile-time constant (ApiService.BASE_URL).