A premium, offline-first mobile fitness application for iOS and Android built for serious lifters. Trainer combines intelligent progressive overload tracking with dynamic rotational scheduling (e.g. Push A / Pull A / Push B / Pull B) to help athletes consistently improve over time.
- Routine & Schedule Engine — Create custom exercises, group them into Routines, and organize Routines into rotating Schedules. The app automatically queues up the next routine based on your active schedule.
- Active Workout Mode — A dedicated, distraction-free UI for logging sets, reps, and weights in real-time.
- Progressive Overload Assistant — Analyses historical data to recommend targets for the current session (e.g. "Add 5 lbs" or "Aim for 1 more rep").
- Comprehensive Analytics — Dashboards tracking total volume, muscle group distribution, workout duration, and estimated 1RMs.
- Holistic Health Tracking — Body weight, daily step count, and other physical activities for a complete fitness picture.
| Layer | Technology |
|---|---|
| Framework | React Native (via Expo ~55) with strict TypeScript ~5.9 |
| Local Database | expo-sqlite (SQLite — offline-first) |
| Global State | Zustand (ephemeral/non-persistent state only) |
| Design Pattern | Feature-Sliced Design (FSD) |
| Styling | NativeWind v4 (Tailwind CSS for React Native) |
| UI Foundation | gluestack-ui (new component library integration path) |
- Node.js ≥ 20
- npm ≥ 10
- Expo CLI —
npm install -g expo-cli - For iOS: Xcode ≥ 15 with a simulator configured
- For Android: Android Studio with an emulator configured (or a physical device with USB debugging)
npm installnpm startThis launches the Expo dev server. From the terminal you can then press:
| Key | Action |
|---|---|
i |
Open iOS Simulator |
a |
Open Android Emulator/Device |
Alternatively, run a platform directly:
npm run ios # iOS Simulator
npm run android # Android Emulator / deviceThe codebase follows Feature-Sliced Design (FSD):
trainer/
├── src/
│ ├── features/ # Self-contained product features
│ │ ├── routines/ # Routine & exercise management
│ │ ├── workout-mode/ # Active workout logging UI
│ │ ├── analytics/ # Volume, 1RM, and progress charts
│ │ ├── schedule/ # Rotating workout schedules
│ │ └── health-tracking/ # Body weight, steps, misc activities
│ │
│ ├── core/ # App-wide infrastructure
│ │ ├── database/ # expo-sqlite schema, types & provider
│ │ ├── navigation/ # Root navigator and route definitions
│ │ └── theme/ # Design tokens (colours, typography)
│ │
│ └── shared/ # Cross-cutting utilities
│ ├── components/ # Reusable primitive UI components
│ ├── hooks/ # Shared React hooks
│ ├── types/ # Cross-cutting TypeScript types
│ └── constants/ # App-wide constants
│
├── App.tsx # Root component
├── index.ts # Expo entry point
├── global.css # NativeWind / Tailwind directives
├── tailwind.config.js # Tailwind theme & NativeWind preset
├── babel.config.js # Babel + module-resolver aliases
├── tsconfig.json # TypeScript strict config + path aliases
├── eslint.config.js # ESLint v9 flat config
└── .prettierrc # Prettier formatting rules
Each feature slice exposes a clean public API through its index.ts and is fully self-contained. Never import directly from another feature's internals.
The app UI is built from the reusable components in src/shared/components.
When updating screens, prefer composing these shared pieces instead of
introducing raw, one-off UI controls.
- Use
Containeras the screen root. - Use
Heading,Muted,Body,Caption, andLabelfor text instead of ad hocTextstyling. - Use
Card,Surface, andDisclosureCardfor grouped content, list rows, and expandable detail panels. - Use
Button,Input,Checkbox, andActionRowfor actions and forms.
DisclosureCardis the default expandable row for schedule and routine summary cards with inline actions and a revealed detail area.ActionRowstandardizes the paired primary/secondary form actions used by create/edit flows.
<Card label="New Schedule">
<Input value={name} onChangeText={setName} placeholder="Schedule name" />
<Label className="mb-2">Select Routines</Label>
{routines.map((routine) => (
<Checkbox
key={routine.id}
checked={selectedIds.includes(routine.id)}
onToggle={() => toggleRoutine(routine.id)}
label={routine.name}
/>
))}
<ActionRow
onPrimaryPress={handleSave}
primaryLoading={saving}
onSecondaryPress={handleCancel}
/>
</Card>When a screen needs a new visual pattern more than once, add it to
src/shared/components and document it here before duplicating markup across
feature screens.
Path aliases are configured in both tsconfig.json and babel.config.js so they work for the TypeScript compiler and at Metro/Babel runtime:
| Alias | Resolves to |
|---|---|
@/* |
src/* |
@features/* |
src/features/* |
@core/* |
src/core/* |
@shared/* |
src/shared/* |
Example usage:
import { colors } from '@core/theme';
import type { RecordId } from '@shared/types';| Script | Description |
|---|---|
npm start |
Start Expo dev server |
npm run ios |
Launch on iOS Simulator |
npm run android |
Launch on Android Emulator / device |
npm run seed:dev |
Start Expo with the development seed set |
npm run lint |
Run ESLint across all .ts/.tsx files |
npm run lint:fix |
Run ESLint and auto-fix issues |
npm run format |
Format all files with Prettier |
npm run format:check |
Check formatting without writing |
npm run type-check |
Type-check without emitting files |
npm run test:perf |
Run deterministic performance contracts |
npm run test:perf:ci |
CI variant of performance contracts |
npm run perf:ios:nightly |
Run iOS simulator nightly perf harness |
npm run perf:baseline:update |
Update committed iOS perf baseline |
npm run seed:dev enables a deterministic development dataset when the app
launches. It seeds the default exercise catalog plus sample routines,
schedules, completed workouts, and body-weight history. The seed is
additive and idempotent, so rerunning the command is safe. The script uses
cross-env, so it works in POSIX shells and Windows shells.
This project uses Husky and lint-staged to automatically enforce code quality on every commit.
| Check | Tool | Scope |
|---|---|---|
| Code formatting | Prettier | Staged .ts, .tsx, .json, .md files |
| Linting | ESLint | Staged .ts/.tsx files |
| Spellcheck | cspell | Staged .md/.txt files |
| Type safety | tsc --noEmit |
Whole project |
| TODO/FIXME check | grep |
Staged .ts/.tsx files |
| Secret scanning | secretlint | All staged files |
| Dependency audit | npm audit --audit-level=high |
Only when package-lock.json is staged |
Commit messages are validated by commitlint using the Conventional Commits spec.
Format: <type>(<optional scope>): <subject>
| Type | When to use |
|---|---|
feat |
A new feature |
fix |
A bug fix |
docs |
Documentation changes only |
style |
Formatting, whitespace — no logic change |
refactor |
Code restructure — no feature or bug change |
perf |
Performance improvements |
test |
Adding or updating tests |
build |
Build system or dependency changes |
ci |
CI configuration changes |
chore |
Maintenance tasks (e.g. updating configs) |
revert |
Reverting a previous commit |
Examples:
feat(workout): add progressive overload suggestions
fix(database): handle missing schedule_id gracefully
docs: update architecture overview
chore: update husky to v9
Hooks are installed automatically when you run npm install (via the prepare lifecycle script). No additional steps are needed.
If hooks do not run after cloning, re-run:
npm run prepare# Skip pre-commit checks for a single commit (not recommended)
git commit --no-verify -m "chore: emergency hotfix"- TypeScript strict mode is enabled.
anyis forbidden — use explicit types. - ESLint (v9 flat config) enforces no-
any, explicit return types, exhaustiveuseEffectdeps, and consistent type imports. - Prettier formats all TypeScript, JSON, and Markdown files: single quotes, trailing commas, 80-char line width, LF line endings.
Run both before pushing:
npm run lint && npm run type-check- Offline-first: All reads and writes go through expo-sqlite. Never block a user action on a network request.
- State separation: Persistent data lives in expo-sqlite; ephemeral/transient state (e.g. active rest timers, in-progress workout flag) lives in Zustand. Never store database records in Zustand.
- Decoupled modules: Progressive overload logic and schedule tracking are built as decoupled modules so they can be swapped or upgraded independently of the UI.
- No business logic in components: Extract domain logic into modules within the relevant feature slice.
For a full breakdown of the data layer, models, state management rules, and conventions for extending the schema, see docs/architecture.md.